Compare commits
36 commits
feat/flatp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e67a04473d | ||
|
|
4914b64b00 | ||
|
|
70c16a66c7 | ||
|
|
e976976dca | ||
|
|
73783a9fb4 | ||
|
|
5cc190a5c0 | ||
|
|
c3e0df4d6c | ||
|
|
360924a260 | ||
|
|
5f087bcb44 | ||
|
|
3a4a985573 | ||
|
|
35da95670d | ||
|
|
dee3c91df1 | ||
|
|
aea8bc54c2 | ||
|
|
0d9a887e0a | ||
|
|
75eaf46e42 | ||
|
|
fb6c5c0686 | ||
|
|
772da89940 | ||
|
|
8116813635 | ||
|
|
34304584f8 | ||
|
|
e1cbb01314 | ||
|
|
a00a13e2fb | ||
|
|
76a42485aa | ||
|
|
17ceb2b382 | ||
|
|
6a281d4c2f | ||
|
|
15605f92e7 | ||
|
|
41275c3ba9 | ||
|
|
20bb565e3d | ||
|
|
ad4b4b1c1b | ||
|
|
f3afbd238f | ||
|
|
edcfe68bf4 | ||
|
|
6703f2543d | ||
|
|
53a6a47c37 | ||
|
|
27e03b1000 | ||
|
|
d338711642 | ||
|
|
45f3e7db5e | ||
|
|
68eba72bf1 |
70 changed files with 2412 additions and 868 deletions
42
.github/workflows/build-release-artifacts.yml
vendored
42
.github/workflows/build-release-artifacts.yml
vendored
|
|
@ -142,14 +142,14 @@ jobs:
|
|||
- name: Build release binary
|
||||
shell: pwsh
|
||||
run: |
|
||||
cargo build -p gitcomet-app --release --locked --features ui-gpui,gix --bins
|
||||
cargo build -p gitcomet --release --locked --features ui-gpui,gix --bins
|
||||
|
||||
- name: Package portable ZIP
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
New-Item -ItemType Directory -Path dist\portable -Force | Out-Null
|
||||
Copy-Item target\release\gitcomet-app.exe dist\portable\gitcomet-app.exe -Force
|
||||
Copy-Item target\release\gitcomet.exe dist\portable\gitcomet.exe -Force
|
||||
Copy-Item README.md dist\portable\README.md -Force
|
||||
Copy-Item LICENSE-AGPL-3.0 dist\portable\LICENSE-AGPL-3.0 -Force
|
||||
Copy-Item NOTICE dist\portable\NOTICE -Force
|
||||
|
|
@ -164,11 +164,11 @@ jobs:
|
|||
$ErrorActionPreference = "Stop"
|
||||
choco install wixtoolset --version "${env:WIXTOOLSET_VERSION}" --yes --no-progress
|
||||
cargo install cargo-wix --version "${env:CARGO_WIX_VERSION}" --locked
|
||||
if (!(Test-Path "crates\gitcomet-app\wix\main.wxs")) {
|
||||
cargo wix init --package gitcomet-app
|
||||
if (!(Test-Path "crates\gitcomet\wix\main.wxs")) {
|
||||
cargo wix init --package gitcomet
|
||||
}
|
||||
$msiName = "gitcomet-v${env:VERSION}-windows-x86_64.msi"
|
||||
cargo wix --package gitcomet-app --profile release --nocapture --no-build --output "dist\$msiName"
|
||||
cargo wix --package gitcomet --profile release --nocapture --no-build --output "dist\$msiName"
|
||||
|
||||
- name: Upload Windows artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
|
|
@ -212,7 +212,7 @@ jobs:
|
|||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build release binary
|
||||
run: cargo build -p gitcomet-app --release --locked --features ui-gpui,gix
|
||||
run: cargo build -p gitcomet --release --locked --features ui-gpui,gix
|
||||
|
||||
- name: Normalize desktop entry metadata
|
||||
run: |
|
||||
|
|
@ -227,7 +227,7 @@ jobs:
|
|||
set -euo pipefail
|
||||
root="gitcomet-v${VERSION}-linux-x86_64"
|
||||
mkdir -p "dist/stage/${root}"
|
||||
install -m755 target/release/gitcomet-app "dist/stage/${root}/gitcomet-app"
|
||||
install -m755 target/release/gitcomet "dist/stage/${root}/gitcomet"
|
||||
install -m644 README.md "dist/stage/${root}/README.md"
|
||||
install -m644 LICENSE-AGPL-3.0 "dist/stage/${root}/LICENSE-AGPL-3.0"
|
||||
install -m644 NOTICE "dist/stage/${root}/NOTICE"
|
||||
|
|
@ -245,7 +245,7 @@ jobs:
|
|||
mkdir -p "${pkg_root}/usr/share/applications"
|
||||
mkdir -p "${pkg_root}/usr/share/icons/hicolor/512x512/apps"
|
||||
|
||||
install -m755 target/release/gitcomet-app "${pkg_root}/usr/bin/gitcomet-app"
|
||||
install -m755 target/release/gitcomet "${pkg_root}/usr/bin/gitcomet"
|
||||
install -m644 dist/gitcomet.desktop "${pkg_root}/usr/share/applications/gitcomet.desktop"
|
||||
install -m644 assets/gitcomet-512.png "${pkg_root}/usr/share/icons/hicolor/512x512/apps/gitcomet.png"
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ jobs:
|
|||
set -euo pipefail
|
||||
appdir="dist/AppDir"
|
||||
mkdir -p "${appdir}/usr/bin"
|
||||
install -m755 target/release/gitcomet-app "${appdir}/usr/bin/gitcomet-app"
|
||||
install -m755 target/release/gitcomet "${appdir}/usr/bin/gitcomet"
|
||||
install -m644 dist/gitcomet.desktop "${appdir}/gitcomet.desktop"
|
||||
install -m644 assets/gitcomet-512.png "${appdir}/gitcomet.png"
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ jobs:
|
|||
echo '#!/usr/bin/env bash'
|
||||
echo 'set -euo pipefail'
|
||||
echo 'HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"'
|
||||
echo 'exec "${HERE}/usr/bin/gitcomet-app" "$@"'
|
||||
echo 'exec "${HERE}/usr/bin/gitcomet" "$@"'
|
||||
} > "${appdir}/AppRun"
|
||||
chmod +x "${appdir}/AppRun"
|
||||
|
||||
|
|
@ -357,6 +357,11 @@ jobs:
|
|||
RELEASE_ID: ${{ needs.prepare.outputs.release_id }}
|
||||
ENABLE_KEYLESS_SIGNING: ${{ vars.ENABLE_KEYLESS_SIGNING }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: windows-release-artifacts
|
||||
|
|
@ -387,18 +392,22 @@ jobs:
|
|||
fi
|
||||
ls -lah dist/release
|
||||
|
||||
- name: Generate Homebrew formula
|
||||
- name: Generate Homebrew cask and formula
|
||||
run: |
|
||||
set -euo pipefail
|
||||
arm_tar="gitcomet-v${VERSION}-macos-arm64.tar.gz"
|
||||
intel_tar="gitcomet-v${VERSION}-macos-x86_64.tar.gz"
|
||||
linux_tar="gitcomet-v${VERSION}-linux-x86_64.tar.gz"
|
||||
arm_dmg="gitcomet-v${VERSION}-macos-arm64.dmg"
|
||||
intel_dmg="gitcomet-v${VERSION}-macos-x86_64.dmg"
|
||||
arm_path="dist/release/${arm_tar}"
|
||||
intel_path="dist/release/${intel_tar}"
|
||||
linux_path="dist/release/${linux_tar}"
|
||||
arm_dmg_path="dist/release/${arm_dmg}"
|
||||
intel_dmg_path="dist/release/${intel_dmg}"
|
||||
|
||||
if [ ! -f "${arm_path}" ] || [ ! -f "${intel_path}" ] || [ ! -f "${linux_path}" ]; then
|
||||
echo "::error title=Missing release tarballs::Expected ${arm_tar}, ${intel_tar}, and ${linux_tar} in dist/release."
|
||||
if [ ! -f "${arm_path}" ] || [ ! -f "${intel_path}" ] || [ ! -f "${linux_path}" ] || [ ! -f "${arm_dmg_path}" ] || [ ! -f "${intel_dmg_path}" ]; then
|
||||
echo "::error title=Missing release assets::Expected ${arm_tar}, ${intel_tar}, ${linux_tar}, ${arm_dmg}, and ${intel_dmg} in dist/release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -408,6 +417,13 @@ jobs:
|
|||
--arm-tar "${arm_path}" \
|
||||
--intel-tar "${intel_path}" \
|
||||
--linux-tar "${linux_path}" \
|
||||
--output "dist/release/gitcomet-cli.rb"
|
||||
|
||||
scripts/generate-homebrew-cask.sh \
|
||||
--version "${VERSION}" \
|
||||
--github-repo "${GITHUB_REPOSITORY}" \
|
||||
--arm-dmg "${arm_dmg_path}" \
|
||||
--intel-dmg "${intel_dmg_path}" \
|
||||
--output "dist/release/gitcomet.rb"
|
||||
|
||||
- name: Generate checksums
|
||||
|
|
|
|||
12
.github/workflows/cross-platform-tests.yml
vendored
12
.github/workflows/cross-platform-tests.yml
vendored
|
|
@ -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 $APP_FEATURES --test mergetool_git_integration gui_default --verbose
|
||||
cargo test -p gitcomet-app $APP_FEATURES --test difftool_git_integration gui_default --verbose
|
||||
cargo test -p gitcomet $APP_FEATURES --test mergetool_git_integration gui_default --verbose
|
||||
cargo test -p gitcomet $APP_FEATURES --test difftool_git_integration gui_default --verbose
|
||||
- name: Run UI smoke test under selected profile
|
||||
run: cargo test -p gitcomet-ui-gpui smoke_tests::smoke_view_renders_without_panicking -- --exact
|
||||
|
||||
|
|
@ -200,9 +200,9 @@ jobs:
|
|||
--verbose
|
||||
- name: Gatekeeper and code-signing check (informational)
|
||||
run: |
|
||||
cargo build -p gitcomet-app $APP_FEATURES --release --locked
|
||||
codesign --verify --verbose target/release/gitcomet-app || true
|
||||
spctl --assess --type execute --verbose target/release/gitcomet-app || true
|
||||
cargo build -p gitcomet $APP_FEATURES --release --locked
|
||||
codesign --verify --verbose target/release/gitcomet || true
|
||||
spctl --assess --type execute --verbose target/release/gitcomet || true
|
||||
|
||||
windows-tests:
|
||||
name: Windows Tests (PowerShell + CMD)
|
||||
|
|
@ -235,7 +235,7 @@ jobs:
|
|||
--verbose
|
||||
- name: Run standalone smoke test in CMD
|
||||
shell: cmd
|
||||
run: cargo test -p gitcomet-app --no-default-features --features gix --test standalone_tool_mode_integration help_flag_exits_zero -- --exact
|
||||
run: cargo test -p gitcomet --no-default-features --features gix --test standalone_tool_mode_integration help_flag_exits_zero -- --exact
|
||||
- name: Show path behavior (CMD + Git Bash)
|
||||
shell: cmd
|
||||
run: |
|
||||
|
|
|
|||
142
.github/workflows/deploy-apt-repo.yml
vendored
142
.github/workflows/deploy-apt-repo.yml
vendored
|
|
@ -21,10 +21,6 @@ on:
|
|||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
repo_base_url:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
distribution:
|
||||
required: false
|
||||
type: string
|
||||
|
|
@ -48,7 +44,7 @@ on:
|
|||
repo_description:
|
||||
required: false
|
||||
type: string
|
||||
default: "GitComet APT repository"
|
||||
default: "GitComet APT repository."
|
||||
container_public_access:
|
||||
required: false
|
||||
type: string
|
||||
|
|
@ -75,32 +71,27 @@ on:
|
|||
required: false
|
||||
type: string
|
||||
storage_account:
|
||||
description: "Azure Storage account name"
|
||||
description: "Azure Storage account name. Defaults to repository variable APT_STORAGE_ACCOUNT when omitted."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
storage_container:
|
||||
description: "Azure Blob container that holds the APT repository"
|
||||
description: "Azure Blob container that holds the APT repository. Defaults to APT_STORAGE_CONTAINER when omitted."
|
||||
required: false
|
||||
default: "apt"
|
||||
type: string
|
||||
storage_prefix:
|
||||
description: "Optional blob prefix within the container"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
repo_base_url:
|
||||
description: "Public HTTPS base URL for the APT repo root"
|
||||
description: "Optional blob prefix within the container. Defaults to APT_STORAGE_PREFIX when omitted."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
distribution:
|
||||
description: "APT suite/codename"
|
||||
description: "APT suite/codename. Defaults to APT_REPO_DISTRIBUTION when omitted."
|
||||
required: false
|
||||
default: "stable"
|
||||
type: string
|
||||
component:
|
||||
description: "APT component"
|
||||
description: "APT component. Defaults to APT_REPO_COMPONENT when omitted."
|
||||
required: false
|
||||
default: "main"
|
||||
type: string
|
||||
|
|
@ -110,22 +101,22 @@ on:
|
|||
default: "amd64"
|
||||
type: string
|
||||
repo_origin:
|
||||
description: "Origin metadata in Release file"
|
||||
description: "Origin metadata in Release file. Defaults to APT_REPO_ORIGIN when omitted."
|
||||
required: false
|
||||
default: "GitComet"
|
||||
type: string
|
||||
repo_label:
|
||||
description: "Label metadata in Release file"
|
||||
description: "Label metadata in Release file. Defaults to APT_REPO_LABEL when omitted."
|
||||
required: false
|
||||
default: "GitComet"
|
||||
type: string
|
||||
repo_description:
|
||||
description: "Description metadata in Release file"
|
||||
description: "Description metadata in Release file. Defaults to APT_REPO_DESCRIPTION when omitted."
|
||||
required: false
|
||||
default: "GitComet APT repository"
|
||||
default: "GitComet APT repository."
|
||||
type: string
|
||||
container_public_access:
|
||||
description: "Public access used only when the blob container must be created"
|
||||
description: "Public access used only when the blob container must be created. Defaults to APT_STORAGE_PUBLIC_ACCESS when omitted."
|
||||
required: false
|
||||
default: "blob"
|
||||
type: choice
|
||||
|
|
@ -165,26 +156,33 @@ jobs:
|
|||
DISPATCH_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_STORAGE_ACCOUNT: ${{ inputs.storage_account }}
|
||||
DISPATCH_STORAGE_ACCOUNT: ${{ github.event.inputs.storage_account }}
|
||||
VAR_STORAGE_ACCOUNT: ${{ vars.APT_STORAGE_ACCOUNT }}
|
||||
INPUT_STORAGE_CONTAINER: ${{ inputs.storage_container }}
|
||||
DISPATCH_STORAGE_CONTAINER: ${{ github.event.inputs.storage_container }}
|
||||
VAR_STORAGE_CONTAINER: ${{ vars.APT_STORAGE_CONTAINER }}
|
||||
INPUT_STORAGE_PREFIX: ${{ inputs.storage_prefix }}
|
||||
DISPATCH_STORAGE_PREFIX: ${{ github.event.inputs.storage_prefix }}
|
||||
INPUT_REPO_BASE_URL: ${{ inputs.repo_base_url }}
|
||||
DISPATCH_REPO_BASE_URL: ${{ github.event.inputs.repo_base_url }}
|
||||
VAR_STORAGE_PREFIX: ${{ vars.APT_STORAGE_PREFIX }}
|
||||
INPUT_DISTRIBUTION: ${{ inputs.distribution }}
|
||||
DISPATCH_DISTRIBUTION: ${{ github.event.inputs.distribution }}
|
||||
VAR_DISTRIBUTION: ${{ vars.APT_REPO_DISTRIBUTION }}
|
||||
INPUT_COMPONENT: ${{ inputs.component }}
|
||||
DISPATCH_COMPONENT: ${{ github.event.inputs.component }}
|
||||
VAR_COMPONENT: ${{ vars.APT_REPO_COMPONENT }}
|
||||
INPUT_ARCHITECTURE: ${{ inputs.architecture }}
|
||||
DISPATCH_ARCHITECTURE: ${{ github.event.inputs.architecture }}
|
||||
INPUT_REPO_ORIGIN: ${{ inputs.repo_origin }}
|
||||
DISPATCH_REPO_ORIGIN: ${{ github.event.inputs.repo_origin }}
|
||||
VAR_REPO_ORIGIN: ${{ vars.APT_REPO_ORIGIN }}
|
||||
INPUT_REPO_LABEL: ${{ inputs.repo_label }}
|
||||
DISPATCH_REPO_LABEL: ${{ github.event.inputs.repo_label }}
|
||||
VAR_REPO_LABEL: ${{ vars.APT_REPO_LABEL }}
|
||||
INPUT_REPO_DESCRIPTION: ${{ inputs.repo_description }}
|
||||
DISPATCH_REPO_DESCRIPTION: ${{ github.event.inputs.repo_description }}
|
||||
VAR_REPO_DESCRIPTION: ${{ vars.APT_REPO_DESCRIPTION }}
|
||||
INPUT_CONTAINER_PUBLIC_ACCESS: ${{ inputs.container_public_access }}
|
||||
DISPATCH_CONTAINER_PUBLIC_ACCESS: ${{ github.event.inputs.container_public_access }}
|
||||
VAR_CONTAINER_PUBLIC_ACCESS: ${{ vars.APT_STORAGE_PUBLIC_ACCESS }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
DISPATCH_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
run: |
|
||||
|
|
@ -192,17 +190,16 @@ jobs:
|
|||
|
||||
tag="${INPUT_TAG:-${DISPATCH_TAG:-}}"
|
||||
version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}"
|
||||
storage_account="${INPUT_STORAGE_ACCOUNT:-${DISPATCH_STORAGE_ACCOUNT:-}}"
|
||||
storage_container="${INPUT_STORAGE_CONTAINER:-${DISPATCH_STORAGE_CONTAINER:-apt}}"
|
||||
storage_prefix="${INPUT_STORAGE_PREFIX:-${DISPATCH_STORAGE_PREFIX:-}}"
|
||||
repo_base_url="${INPUT_REPO_BASE_URL:-${DISPATCH_REPO_BASE_URL:-}}"
|
||||
distribution="${INPUT_DISTRIBUTION:-${DISPATCH_DISTRIBUTION:-stable}}"
|
||||
component="${INPUT_COMPONENT:-${DISPATCH_COMPONENT:-main}}"
|
||||
storage_account="${INPUT_STORAGE_ACCOUNT:-${DISPATCH_STORAGE_ACCOUNT:-${VAR_STORAGE_ACCOUNT:-}}}"
|
||||
storage_container="${INPUT_STORAGE_CONTAINER:-${DISPATCH_STORAGE_CONTAINER:-${VAR_STORAGE_CONTAINER:-apt}}}"
|
||||
storage_prefix="${INPUT_STORAGE_PREFIX:-${DISPATCH_STORAGE_PREFIX:-${VAR_STORAGE_PREFIX:-}}}"
|
||||
distribution="${INPUT_DISTRIBUTION:-${DISPATCH_DISTRIBUTION:-${VAR_DISTRIBUTION:-stable}}}"
|
||||
component="${INPUT_COMPONENT:-${DISPATCH_COMPONENT:-${VAR_COMPONENT:-main}}}"
|
||||
architecture="${INPUT_ARCHITECTURE:-${DISPATCH_ARCHITECTURE:-amd64}}"
|
||||
repo_origin="${INPUT_REPO_ORIGIN:-${DISPATCH_REPO_ORIGIN:-GitComet}}"
|
||||
repo_label="${INPUT_REPO_LABEL:-${DISPATCH_REPO_LABEL:-GitComet}}"
|
||||
repo_description="${INPUT_REPO_DESCRIPTION:-${DISPATCH_REPO_DESCRIPTION:-GitComet APT repository}}"
|
||||
container_public_access="${INPUT_CONTAINER_PUBLIC_ACCESS:-${DISPATCH_CONTAINER_PUBLIC_ACCESS:-blob}}"
|
||||
repo_origin="${INPUT_REPO_ORIGIN:-${DISPATCH_REPO_ORIGIN:-${VAR_REPO_ORIGIN:-GitComet}}}"
|
||||
repo_label="${INPUT_REPO_LABEL:-${DISPATCH_REPO_LABEL:-${VAR_REPO_LABEL:-GitComet}}}"
|
||||
repo_description="${INPUT_REPO_DESCRIPTION:-${DISPATCH_REPO_DESCRIPTION:-${VAR_REPO_DESCRIPTION:-GitComet APT repository.}}}"
|
||||
container_public_access="${INPUT_CONTAINER_PUBLIC_ACCESS:-${DISPATCH_CONTAINER_PUBLIC_ACCESS:-${VAR_CONTAINER_PUBLIC_ACCESS:-blob}}}"
|
||||
dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}"
|
||||
|
||||
tag="$(echo "$tag" | tr -d '[:space:]')"
|
||||
|
|
@ -210,7 +207,6 @@ jobs:
|
|||
storage_account="$(echo "$storage_account" | tr -d '[:space:]')"
|
||||
storage_container="$(echo "$storage_container" | tr -d '[:space:]')"
|
||||
storage_prefix="$(echo "$storage_prefix" | sed 's#^/*##;s#/*$##')"
|
||||
repo_base_url="$(echo "$repo_base_url" | sed 's#[[:space:]]##g;s#/*$##')"
|
||||
distribution="$(echo "$distribution" | tr -d '[:space:]')"
|
||||
component="$(echo "$component" | tr -d '[:space:]')"
|
||||
architecture="$(echo "$architecture" | tr -d '[:space:]')"
|
||||
|
|
@ -274,7 +270,8 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$repo_base_url" ] && [ -n "$storage_account" ] && [ "$storage_container" != '$web' ]; then
|
||||
repo_base_url=""
|
||||
if [ -n "$storage_account" ] && [ "$storage_container" != '$web' ]; then
|
||||
repo_base_url="https://${storage_account}.blob.core.windows.net/${storage_container}"
|
||||
if [ -n "$storage_prefix" ]; then
|
||||
repo_base_url="${repo_base_url}/${storage_prefix}"
|
||||
|
|
@ -365,7 +362,7 @@ jobs:
|
|||
echo "Key-Type: RSA"
|
||||
echo "Key-Length: 3072"
|
||||
echo "Name-Real: GitComet APT Dry Run"
|
||||
echo "Name-Email: ci@example.invalid"
|
||||
echo "Name-Email: ci@autoexplore.ai"
|
||||
echo "Expire-Date: 0"
|
||||
echo "%no-protection"
|
||||
echo "%commit"
|
||||
|
|
@ -419,16 +416,16 @@ jobs:
|
|||
blob_prefix="${blob_prefix}/"
|
||||
fi
|
||||
|
||||
mapfile -t existing_blobs < <(
|
||||
az storage blob list \
|
||||
--account-name "$STORAGE_ACCOUNT" \
|
||||
--container-name "$STORAGE_CONTAINER" \
|
||||
--account-key "$AZURE_STORAGE_ACCOUNT_KEY" \
|
||||
--prefix "$blob_prefix" \
|
||||
--num-results "*" \
|
||||
--query "[].name" \
|
||||
-o tsv
|
||||
)
|
||||
existing_blobs_file="${RUNNER_TEMP}/apt-existing-blobs.txt"
|
||||
az storage blob list \
|
||||
--account-name "$STORAGE_ACCOUNT" \
|
||||
--container-name "$STORAGE_CONTAINER" \
|
||||
--account-key "$AZURE_STORAGE_ACCOUNT_KEY" \
|
||||
--prefix "$blob_prefix" \
|
||||
--num-results "*" \
|
||||
--query "[].name" \
|
||||
-o tsv > "$existing_blobs_file"
|
||||
mapfile -t existing_blobs < "$existing_blobs_file"
|
||||
|
||||
if [ "${#existing_blobs[@]}" -eq 0 ]; then
|
||||
exit 0
|
||||
|
|
@ -472,6 +469,7 @@ jobs:
|
|||
--label "${{ steps.norm.outputs.repo_label }}"
|
||||
--description "${{ steps.norm.outputs.repo_description }}"
|
||||
--signing-key "${{ steps.gpg.outputs.signing_key }}"
|
||||
--repo-url "https://apt.gitcomet.dev"
|
||||
)
|
||||
|
||||
if [ -n "${{ steps.norm.outputs.repo_base_url }}" ]; then
|
||||
|
|
@ -495,6 +493,9 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
metadata_cache_control='no-cache, no-store, max-age=0, must-revalidate'
|
||||
package_cache_control='public, max-age=31536000, immutable'
|
||||
|
||||
mkdir -p "$AZURE_CONFIG_DIR"
|
||||
: > dist/apt-uploaded-blobs.txt
|
||||
|
||||
|
|
@ -505,15 +506,25 @@ jobs:
|
|||
blob_name="${STORAGE_PREFIX}/${rel_path}"
|
||||
fi
|
||||
|
||||
az storage blob upload \
|
||||
--account-name "$STORAGE_ACCOUNT" \
|
||||
--container-name "$STORAGE_CONTAINER" \
|
||||
--account-key "$AZURE_STORAGE_ACCOUNT_KEY" \
|
||||
--name "$blob_name" \
|
||||
--file "$file_path" \
|
||||
--overwrite true \
|
||||
--no-progress \
|
||||
upload_args=(
|
||||
az storage blob upload
|
||||
--account-name "$STORAGE_ACCOUNT"
|
||||
--container-name "$STORAGE_CONTAINER"
|
||||
--account-key "$AZURE_STORAGE_ACCOUNT_KEY"
|
||||
--name "$blob_name"
|
||||
--file "$file_path"
|
||||
--overwrite true
|
||||
--no-progress
|
||||
--output none
|
||||
)
|
||||
|
||||
if [[ "$rel_path" == dists/* || "$rel_path" == "gitcomet.sources" || "$rel_path" == "gitcomet.list" || "$rel_path" == "gitcomet-archive-keyring.gpg" || "$rel_path" == "gitcomet-archive-keyring.asc" || "$rel_path" == "README.txt" ]]; then
|
||||
upload_args+=(--content-cache-control "$metadata_cache_control")
|
||||
elif [[ "$rel_path" == pool/* ]]; then
|
||||
upload_args+=(--content-cache-control "$package_cache_control")
|
||||
fi
|
||||
|
||||
"${upload_args[@]}"
|
||||
|
||||
printf '%s\n' "$blob_name" >> dist/apt-uploaded-blobs.txt
|
||||
done < <(find dist/apt-repo -type f -print0 | sort -z)
|
||||
|
|
@ -549,28 +560,3 @@ jobs:
|
|||
--delete-snapshots include \
|
||||
--output none
|
||||
done < "${RUNNER_TEMP}/apt-stale.txt"
|
||||
|
||||
- name: Emit deployment summary
|
||||
run: |
|
||||
set -euo pipefail
|
||||
uploaded="yes"
|
||||
if [ "${{ steps.norm.outputs.dry_run }}" = "true" ]; then
|
||||
uploaded="no (dry run)"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "### APT repository deployment"
|
||||
echo ""
|
||||
echo "- Release: \`${{ steps.norm.outputs.tag }}\`"
|
||||
echo "- Debian package: \`${{ steps.deb.outputs.name }}\`"
|
||||
echo "- Distribution: \`${{ steps.norm.outputs.distribution }}\`"
|
||||
echo "- Component: \`${{ steps.norm.outputs.component }}\`"
|
||||
echo "- Architecture: \`${{ steps.norm.outputs.architecture }}\`"
|
||||
echo "- Azure target: \`${{ steps.norm.outputs.storage_account }}/${{ steps.norm.outputs.storage_container }}\`"
|
||||
echo "- Prefix: \`${{ steps.norm.outputs.storage_prefix }}\`"
|
||||
echo "- Public repo URL: \`${{ steps.norm.outputs.repo_base_url }}\`"
|
||||
echo "- Uploaded: \`${uploaded}\`"
|
||||
echo ""
|
||||
echo "Generated files:"
|
||||
find dist/apt-repo -type f | sed 's#^dist/apt-repo/#- `#;s#$#`#'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
|
|
|||
268
.github/workflows/deploy-aur.yml
vendored
Normal file
268
.github/workflows/deploy-aur.yml
vendored
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
name: Deploy AUR Mirror
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
aur_repo:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
aur_branch:
|
||||
required: false
|
||||
type: string
|
||||
default: "main"
|
||||
dry_run:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
AUR_REPO_TOKEN:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release version (e.g. 0.2.0 or v0.2.0)"
|
||||
required: true
|
||||
type: string
|
||||
tag:
|
||||
description: "Optional tag override (e.g. v0.2.0). Defaults to v<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"
|
||||
49
.github/workflows/deploy-homebrew-tap.yml
vendored
49
.github/workflows/deploy-homebrew-tap.yml
vendored
|
|
@ -35,17 +35,17 @@ on:
|
|||
required: false
|
||||
type: string
|
||||
tap_repo:
|
||||
description: "Target tap repository in OWNER/REPO form (e.g. Auto-Explore/homebrew-gitcomet)"
|
||||
description: "Target tap repository in OWNER/REPO form (e.g. Auto-Explore/homebrew-gitcomet). Defaults to HOMEBREW_TAP_REPO when omitted."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
tap_branch:
|
||||
description: "Target branch in tap repo"
|
||||
description: "Target branch in tap repo. Defaults to HOMEBREW_TAP_BRANCH when omitted."
|
||||
required: false
|
||||
default: "main"
|
||||
type: string
|
||||
dry_run:
|
||||
description: "Validate and print formula without pushing to tap"
|
||||
description: "Validate and print cask/formula without pushing to tap"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
|
@ -59,7 +59,7 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Publish formula to Homebrew tap
|
||||
name: Publish Homebrew cask and formula
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
|
|
@ -74,8 +74,10 @@ jobs:
|
|||
DISPATCH_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_TAP_REPO: ${{ inputs.tap_repo }}
|
||||
DISPATCH_TAP_REPO: ${{ github.event.inputs.tap_repo }}
|
||||
VAR_TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO }}
|
||||
INPUT_TAP_BRANCH: ${{ inputs.tap_branch }}
|
||||
DISPATCH_TAP_BRANCH: ${{ github.event.inputs.tap_branch }}
|
||||
VAR_TAP_BRANCH: ${{ vars.HOMEBREW_TAP_BRANCH }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
DISPATCH_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
|
|
@ -84,8 +86,8 @@ jobs:
|
|||
|
||||
tag="${INPUT_TAG:-${DISPATCH_TAG:-}}"
|
||||
version="${INPUT_VERSION:-${DISPATCH_VERSION:-}}"
|
||||
tap_repo="${INPUT_TAP_REPO:-${DISPATCH_TAP_REPO:-}}"
|
||||
tap_branch="${INPUT_TAP_BRANCH:-${DISPATCH_TAP_BRANCH:-main}}"
|
||||
tap_repo="${INPUT_TAP_REPO:-${DISPATCH_TAP_REPO:-${VAR_TAP_REPO:-}}}"
|
||||
tap_branch="${INPUT_TAP_BRANCH:-${DISPATCH_TAP_BRANCH:-${VAR_TAP_BRANCH:-main}}}"
|
||||
dry_run="${INPUT_DRY_RUN:-${DISPATCH_DRY_RUN:-false}}"
|
||||
|
||||
tag="$(echo "$tag" | tr -d '[:space:]')"
|
||||
|
|
@ -138,7 +140,7 @@ jobs:
|
|||
echo "tap_branch=$tap_branch" >> "$GITHUB_OUTPUT"
|
||||
echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download formula from release assets
|
||||
- name: Download cask and formula from release assets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist
|
||||
|
|
@ -146,12 +148,17 @@ jobs:
|
|||
gh release download "${{ steps.norm.outputs.tag }}" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "gitcomet.rb" \
|
||||
--pattern "gitcomet-cli.rb" \
|
||||
--dir dist \
|
||||
--clobber
|
||||
test -f dist/gitcomet.rb
|
||||
test -f dist/gitcomet-cli.rb
|
||||
|
||||
- name: Validate formula syntax
|
||||
run: ruby -c dist/gitcomet.rb
|
||||
- name: Validate cask and formula syntax
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ruby -c dist/gitcomet.rb
|
||||
ruby -c dist/gitcomet-cli.rb
|
||||
|
||||
- name: Emit dry-run summary
|
||||
if: ${{ steps.norm.outputs.dry_run == 'true' }}
|
||||
|
|
@ -164,13 +171,18 @@ jobs:
|
|||
echo "- Target tap: \`${{ steps.norm.outputs.tap_repo }}\`"
|
||||
echo "- Target branch: \`${{ steps.norm.outputs.tap_branch }}\`"
|
||||
echo ""
|
||||
echo "Formula preview:"
|
||||
echo "Cask preview (Casks/gitcomet.rb):"
|
||||
echo '```ruby'
|
||||
cat dist/gitcomet.rb
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "Formula preview (Formula/gitcomet-cli.rb):"
|
||||
echo '```ruby'
|
||||
cat dist/gitcomet-cli.rb
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Publish formula to tap repository
|
||||
- name: Publish cask and formula to tap repository
|
||||
if: ${{ steps.norm.outputs.dry_run != 'true' }}
|
||||
env:
|
||||
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
|
|
@ -187,19 +199,24 @@ jobs:
|
|||
|
||||
rm -rf tap-repo
|
||||
git clone "https://x-access-token:${TAP_TOKEN}@github.com/${TAP_REPO}.git" tap-repo
|
||||
mkdir -p tap-repo/Formula
|
||||
cp dist/gitcomet.rb tap-repo/Formula/gitcomet.rb
|
||||
mkdir -p tap-repo/Casks tap-repo/Formula
|
||||
cp dist/gitcomet.rb tap-repo/Casks/gitcomet.rb
|
||||
cp dist/gitcomet-cli.rb tap-repo/Formula/gitcomet-cli.rb
|
||||
|
||||
pushd tap-repo >/dev/null
|
||||
if git diff --quiet -- Formula/gitcomet.rb; then
|
||||
echo "No formula changes detected; tap already up to date."
|
||||
git add Casks/gitcomet.rb Formula/gitcomet-cli.rb
|
||||
if git ls-files --error-unmatch Formula/gitcomet.rb >/dev/null 2>&1; then
|
||||
git rm -f Formula/gitcomet.rb
|
||||
fi
|
||||
|
||||
if git diff --cached --quiet -- Casks/gitcomet.rb Formula/gitcomet-cli.rb Formula/gitcomet.rb; then
|
||||
echo "No Homebrew cask/formula changes detected; tap already up to date."
|
||||
popd >/dev/null
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add Formula/gitcomet.rb
|
||||
git commit -m "gitcomet ${TAG}"
|
||||
git push origin "HEAD:${TAP_BRANCH}"
|
||||
popd >/dev/null
|
||||
|
|
|
|||
217
.github/workflows/deployment-ci.yml
vendored
217
.github/workflows/deployment-ci.yml
vendored
|
|
@ -5,21 +5,27 @@ on:
|
|||
branches: ["main"]
|
||||
paths:
|
||||
- ".github/workflows/build-release-artifacts.yml"
|
||||
- ".github/workflows/deploy-aur.yml"
|
||||
- ".github/workflows/deploy-homebrew-tap.yml"
|
||||
- ".github/workflows/deploy-apt-repo.yml"
|
||||
- ".github/workflows/release-manual-main.yml"
|
||||
- "scripts/update-aur.sh"
|
||||
- "scripts/package-macos.sh"
|
||||
- "scripts/generate-homebrew-formula.sh"
|
||||
- "scripts/generate-homebrew-cask.sh"
|
||||
- "scripts/build-apt-repo.sh"
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- ".github/workflows/build-release-artifacts.yml"
|
||||
- ".github/workflows/deploy-aur.yml"
|
||||
- ".github/workflows/deploy-homebrew-tap.yml"
|
||||
- ".github/workflows/deploy-apt-repo.yml"
|
||||
- ".github/workflows/release-manual-main.yml"
|
||||
- "scripts/update-aur.sh"
|
||||
- "scripts/package-macos.sh"
|
||||
- "scripts/generate-homebrew-formula.sh"
|
||||
- "scripts/generate-homebrew-cask.sh"
|
||||
- "scripts/build-apt-repo.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
@ -39,27 +45,58 @@ jobs:
|
|||
|
||||
- name: Validate shell scripts syntax
|
||||
run: |
|
||||
bash -n scripts/update-aur.sh
|
||||
bash -n scripts/package-macos.sh
|
||||
bash -n scripts/generate-homebrew-formula.sh
|
||||
bash -n scripts/generate-homebrew-cask.sh
|
||||
bash -n scripts/build-apt-repo.sh
|
||||
|
||||
- name: Validate workflow YAML
|
||||
run: |
|
||||
ruby -e 'require "yaml"; %w[
|
||||
.github/workflows/build-release-artifacts.yml
|
||||
.github/workflows/deploy-aur.yml
|
||||
.github/workflows/deploy-homebrew-tap.yml
|
||||
.github/workflows/deploy-apt-repo.yml
|
||||
.github/workflows/deployment-ci.yml
|
||||
.github/workflows/release-manual-main.yml
|
||||
].each { |path| YAML.load_file(path) }'
|
||||
|
||||
- name: Generate formula from synthetic artifacts
|
||||
- name: Validate deployment workflow config keys
|
||||
run: |
|
||||
set -euo pipefail
|
||||
grep -Fq 'vars.AUR_GITHUB_REPO' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.AUR_GITHUB_BRANCH' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_STORAGE_ACCOUNT' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_STORAGE_CONTAINER' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_REPO_DISTRIBUTION' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_REPO_COMPONENT' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_REPO_ORIGIN' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_REPO_LABEL' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_REPO_DESCRIPTION' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_STORAGE_PUBLIC_ACCESS' .github/workflows/release-manual-main.yml
|
||||
grep -Fq 'vars.APT_STORAGE_ACCOUNT' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.APT_STORAGE_CONTAINER' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.APT_REPO_DISTRIBUTION' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.APT_REPO_COMPONENT' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.APT_REPO_ORIGIN' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.APT_REPO_LABEL' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.APT_REPO_DESCRIPTION' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.APT_STORAGE_PUBLIC_ACCESS' .github/workflows/deploy-apt-repo.yml
|
||||
grep -Fq 'vars.AUR_GITHUB_REPO' .github/workflows/deploy-aur.yml
|
||||
grep -Fq 'vars.AUR_GITHUB_BRANCH' .github/workflows/deploy-aur.yml
|
||||
grep -Fq 'AUR_REPO_TOKEN' .github/workflows/deploy-aur.yml
|
||||
grep -Fq 'HOMEBREW_TAP_TOKEN' .github/workflows/deploy-homebrew-tap.yml
|
||||
|
||||
- name: Generate Homebrew cask and formula from synthetic artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist/deployment-ci
|
||||
printf 'arm64-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.tar.gz
|
||||
printf 'intel-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.tar.gz
|
||||
printf 'linux-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-linux-x86_64.tar.gz
|
||||
printf 'arm64-dmg-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.dmg
|
||||
printf 'intel-dmg-test' > dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.dmg
|
||||
|
||||
scripts/generate-homebrew-formula.sh \
|
||||
--version "0.0.0-ci" \
|
||||
|
|
@ -67,11 +104,137 @@ jobs:
|
|||
--arm-tar "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.tar.gz" \
|
||||
--intel-tar "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.tar.gz" \
|
||||
--linux-tar "dist/deployment-ci/gitcomet-v0.0.0-ci-linux-x86_64.tar.gz" \
|
||||
--output "dist/deployment-ci/gitcomet-cli.rb"
|
||||
|
||||
scripts/generate-homebrew-cask.sh \
|
||||
--version "0.0.0-ci" \
|
||||
--github-repo "Auto-Explore/GitComet" \
|
||||
--arm-dmg "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-arm64.dmg" \
|
||||
--intel-dmg "dist/deployment-ci/gitcomet-v0.0.0-ci-macos-x86_64.dmg" \
|
||||
--output "dist/deployment-ci/gitcomet.rb"
|
||||
|
||||
ruby -c dist/deployment-ci/gitcomet.rb
|
||||
grep -q "on_linux do" dist/deployment-ci/gitcomet.rb
|
||||
grep -q "gitcomet-v0.0.0-ci-linux-x86_64.tar.gz" dist/deployment-ci/gitcomet.rb
|
||||
ruby -c dist/deployment-ci/gitcomet-cli.rb
|
||||
grep -q 'cask "gitcomet" do' dist/deployment-ci/gitcomet.rb
|
||||
grep -q 'app "GitComet.app"' dist/deployment-ci/gitcomet.rb
|
||||
grep -q 'brew install gitcomet-cli' dist/deployment-ci/gitcomet.rb
|
||||
grep -q 'class GitcometCli < Formula' dist/deployment-ci/gitcomet-cli.rb
|
||||
grep -q "on_linux do" dist/deployment-ci/gitcomet-cli.rb
|
||||
grep -q 'bin.install "gitcomet"' dist/deployment-ci/gitcomet-cli.rb
|
||||
|
||||
- name: Validate Homebrew tap publish detection
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf dist/deployment-ci/tap-repo
|
||||
git init --initial-branch=main dist/deployment-ci/tap-repo
|
||||
|
||||
pushd dist/deployment-ci/tap-repo >/dev/null
|
||||
git config user.name "GitComet CI"
|
||||
git config user.email "ci@autoexplore.ai"
|
||||
echo "# Homebrew Tap" > README.md
|
||||
mkdir -p Formula
|
||||
cat > Formula/gitcomet.rb <<'RUBY'
|
||||
class Gitcomet < Formula
|
||||
desc "Legacy GitComet formula"
|
||||
homepage "https://example.com"
|
||||
url "https://example.com/gitcomet.tar.gz"
|
||||
sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
version "0.0.0"
|
||||
end
|
||||
RUBY
|
||||
|
||||
git add README.md Formula/gitcomet.rb
|
||||
git commit -m "Initial tap repo"
|
||||
|
||||
mkdir -p Casks Formula
|
||||
cp ../gitcomet.rb Casks/gitcomet.rb
|
||||
cp ../gitcomet-cli.rb Formula/gitcomet-cli.rb
|
||||
|
||||
git add Casks/gitcomet.rb Formula/gitcomet-cli.rb
|
||||
if git ls-files --error-unmatch Formula/gitcomet.rb >/dev/null 2>&1; then
|
||||
git rm -f Formula/gitcomet.rb
|
||||
fi
|
||||
|
||||
if git diff --cached --quiet -- Casks/gitcomet.rb Formula/gitcomet-cli.rb Formula/gitcomet.rb; then
|
||||
echo "Expected Homebrew cask/formula publish to be detected as a change."
|
||||
exit 1
|
||||
fi
|
||||
popd >/dev/null
|
||||
|
||||
aur-metadata-smoke:
|
||||
name: Validate AUR metadata update script
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
container:
|
||||
image: archlinux:base-devel
|
||||
steps:
|
||||
- name: Install Arch packaging tooling
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pacman -Sy --noconfirm --needed perl shadow
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Create non-root packaging user
|
||||
run: |
|
||||
set -euo pipefail
|
||||
id -u builder >/dev/null 2>&1 || useradd -m builder
|
||||
chown -R builder:builder "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Prepare synthetic AUR repo and assets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist/aur-ci/repo
|
||||
cat > dist/aur-ci/repo/PKGBUILD <<'EOF'
|
||||
pkgname=gitcomet
|
||||
pkgver=0.0.0
|
||||
pkgrel=1
|
||||
pkgdesc="Fast, resource-efficient Git GUI written in Rust"
|
||||
arch=('x86_64')
|
||||
url="https://gitcomet.dev/"
|
||||
license=('AGPL-3.0-only')
|
||||
depends=(
|
||||
'fontconfig'
|
||||
'freetype2'
|
||||
'git'
|
||||
'libx11'
|
||||
'libxcb'
|
||||
'libxkbcommon'
|
||||
'wayland'
|
||||
)
|
||||
source=(
|
||||
"gitcomet-v$pkgver-linux-x86_64.tar.gz::https://example.invalid/gitcomet-v$pkgver-linux-x86_64.tar.gz"
|
||||
"gitcomet-source-v$pkgver.tar.gz::https://example.invalid/gitcomet-source-v$pkgver.tar.gz"
|
||||
)
|
||||
sha256sums=('oldbinary'
|
||||
'oldsource')
|
||||
|
||||
package() {
|
||||
install -D -m755 "gitcomet-v$pkgver-linux-x86_64/gitcomet" "$pkgdir/usr/bin/gitcomet"
|
||||
}
|
||||
EOF
|
||||
|
||||
printf 'synthetic-binary' > dist/aur-ci/gitcomet-v0.0.0-linux-x86_64.tar.gz
|
||||
printf 'synthetic-source' > dist/aur-ci/gitcomet-source-v0.0.0.tar.gz
|
||||
chown -R builder:builder dist
|
||||
|
||||
- name: Run update-aur.sh
|
||||
run: |
|
||||
set -euo pipefail
|
||||
su builder -c "cd '$GITHUB_WORKSPACE' && scripts/update-aur.sh \
|
||||
--aur-dir '$GITHUB_WORKSPACE/dist/aur-ci/repo' \
|
||||
--version 0.0.0 \
|
||||
--binary-tar '$GITHUB_WORKSPACE/dist/aur-ci/gitcomet-v0.0.0-linux-x86_64.tar.gz' \
|
||||
--source-tar '$GITHUB_WORKSPACE/dist/aur-ci/gitcomet-source-v0.0.0.tar.gz' \
|
||||
--verify-source"
|
||||
|
||||
- name: Validate generated PKGBUILD and .SRCINFO
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f dist/aur-ci/repo/.SRCINFO
|
||||
grep -q '^pkgver=0.0.0$' dist/aur-ci/repo/PKGBUILD
|
||||
grep -q 'source = gitcomet-v0.0.0-linux-x86_64.tar.gz::https://example.invalid/gitcomet-v0.0.0-linux-x86_64.tar.gz$' dist/aur-ci/repo/.SRCINFO
|
||||
grep -q 'source = gitcomet-source-v0.0.0.tar.gz::https://example.invalid/gitcomet-source-v0.0.0.tar.gz$' dist/aur-ci/repo/.SRCINFO
|
||||
|
||||
apt-repo-generation-smoke:
|
||||
name: Validate APT repo generation script
|
||||
|
|
@ -91,14 +254,14 @@ jobs:
|
|||
pkg_root="dist/apt-ci/pkg"
|
||||
mkdir -p "${pkg_root}/DEBIAN" "${pkg_root}/usr/bin"
|
||||
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'echo "GitComet CI"' > "${pkg_root}/usr/bin/gitcomet-app"
|
||||
chmod +x "${pkg_root}/usr/bin/gitcomet-app"
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'echo "GitComet CI"' > "${pkg_root}/usr/bin/gitcomet"
|
||||
chmod +x "${pkg_root}/usr/bin/gitcomet"
|
||||
|
||||
{
|
||||
echo "Package: gitcomet"
|
||||
echo "Version: 0.0.0~ci1"
|
||||
echo "Architecture: amd64"
|
||||
echo "Maintainer: GitComet CI <ci@example.invalid>"
|
||||
echo "Maintainer: GitComet CI <ci@autoexplore.ai>"
|
||||
echo "Description: Synthetic GitComet package for deployment CI."
|
||||
} > "${pkg_root}/DEBIAN/control"
|
||||
|
||||
|
|
@ -116,7 +279,7 @@ jobs:
|
|||
echo "Key-Type: RSA"
|
||||
echo "Key-Length: 3072"
|
||||
echo "Name-Real: GitComet CI"
|
||||
echo "Name-Email: ci@example.invalid"
|
||||
echo "Name-Email: ci@autoexplore.ai"
|
||||
echo "Expire-Date: 0"
|
||||
echo "%no-protection"
|
||||
echo "%commit"
|
||||
|
|
@ -129,6 +292,7 @@ jobs:
|
|||
- name: Build signed APT repo
|
||||
env:
|
||||
GNUPGHOME: ${{ github.workspace }}/dist/apt-ci/gnupg
|
||||
APT_GPG_PASSPHRASE: ${{ secrets.APT_GPG_PASSPHRASE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build-apt-repo.sh \
|
||||
|
|
@ -141,6 +305,7 @@ jobs:
|
|||
--label GitComet \
|
||||
--description "GitComet deployment CI repository" \
|
||||
--repo-url https://example.invalid/gitcomet/apt \
|
||||
--gpg-passphrase "${APT_GPG_PASSPHRASE}" \
|
||||
--signing-key "${APT_TEST_GPG_KEY_ID}"
|
||||
|
||||
- name: Validate generated repo
|
||||
|
|
@ -163,6 +328,38 @@ jobs:
|
|||
chmod 700 "$GNUPGHOME"
|
||||
gpg --batch --no-default-keyring --keyring dist/apt-ci/repo/gitcomet-archive-keyring.gpg --verify dist/apt-ci/repo/dists/stable/InRelease >/dev/null
|
||||
|
||||
- name: Verify generated repo with apt-secure
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(pwd)/dist/apt-ci/repo"
|
||||
source_dir="${RUNNER_TEMP}/gitcomet-apt-sources.d"
|
||||
lists_dir="${RUNNER_TEMP}/gitcomet-apt-lists"
|
||||
|
||||
mkdir -p "$source_dir"
|
||||
sudo mkdir -p "${lists_dir}/partial"
|
||||
|
||||
cat > "${source_dir}/gitcomet-ci.sources" <<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 }}
|
||||
|
|
@ -185,7 +382,7 @@ jobs:
|
|||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build release binary
|
||||
run: cargo build -p gitcomet-app --release --locked --features ui-gpui,gix --bins
|
||||
run: cargo build -p gitcomet --release --locked --features ui-gpui,gix --bins
|
||||
|
||||
- name: Package macOS artifacts
|
||||
run: |
|
||||
|
|
@ -202,7 +399,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-app$"
|
||||
tar -tzf "$tarball" | grep -q "GitComet.app/Contents/MacOS/gitcomet$"
|
||||
|
||||
- name: Verify DMG launches binary
|
||||
run: |
|
||||
|
|
@ -216,5 +413,5 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
"${mount_point}/GitComet.app/Contents/MacOS/gitcomet-app" --help >/dev/null
|
||||
"${mount_point}/GitComet.app/Contents/MacOS/gitcomet" --help >/dev/null
|
||||
hdiutil detach "$mount_point"
|
||||
|
|
|
|||
20
.github/workflows/release-manual-main.yml
vendored
20
.github/workflows/release-manual-main.yml
vendored
|
|
@ -82,10 +82,10 @@ jobs:
|
|||
- name: Verify crate version matches release version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
crate_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' crates/gitcomet-app/Cargo.toml | head -n1)"
|
||||
crate_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
wanted="${{ steps.norm.outputs.version }}"
|
||||
if [ "$crate_version" != "$wanted" ]; then
|
||||
echo "::error title=Version mismatch::crates/gitcomet-app/Cargo.toml version '$crate_version' does not match requested release '$wanted'."
|
||||
echo "::error title=Version mismatch::Cargo.toml version '$crate_version' does not match requested release '$wanted'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ jobs:
|
|||
gh release view "$tag" --repo "$GITHUB_REPOSITORY"
|
||||
|
||||
deploy_homebrew_tap:
|
||||
name: Deploy Homebrew tap formula
|
||||
name: Deploy Homebrew tap packages
|
||||
needs: [validate, build_and_upload, publish_release]
|
||||
if: ${{ fromJSON(needs.validate.outputs.draft) == false && needs.build_and_upload.result == 'success' }}
|
||||
uses: ./.github/workflows/deploy-homebrew-tap.yml
|
||||
|
|
@ -187,6 +187,19 @@ jobs:
|
|||
dry_run: false
|
||||
secrets: inherit
|
||||
|
||||
deploy_aur:
|
||||
name: Deploy AUR mirror metadata
|
||||
needs: [validate, build_and_upload, publish_release]
|
||||
if: ${{ fromJSON(needs.validate.outputs.draft) == false && needs.build_and_upload.result == 'success' }}
|
||||
uses: ./.github/workflows/deploy-aur.yml
|
||||
with:
|
||||
tag: ${{ needs.validate.outputs.tag }}
|
||||
version: ${{ needs.validate.outputs.version }}
|
||||
aur_repo: ${{ vars.AUR_GITHUB_REPO }}
|
||||
aur_branch: ${{ vars.AUR_GITHUB_BRANCH }}
|
||||
dry_run: false
|
||||
secrets: inherit
|
||||
|
||||
deploy_apt_repo:
|
||||
name: Deploy Azure APT repository
|
||||
needs: [validate, build_and_upload, publish_release]
|
||||
|
|
@ -198,7 +211,6 @@ jobs:
|
|||
storage_account: ${{ vars.APT_STORAGE_ACCOUNT }}
|
||||
storage_container: ${{ vars.APT_STORAGE_CONTAINER }}
|
||||
storage_prefix: ${{ vars.APT_STORAGE_PREFIX }}
|
||||
repo_base_url: ${{ vars.APT_REPO_BASE_URL }}
|
||||
distribution: ${{ vars.APT_REPO_DISTRIBUTION }}
|
||||
component: ${{ vars.APT_REPO_COMPONENT }}
|
||||
architecture: "amd64"
|
||||
|
|
|
|||
18
.github/workflows/rust.yml
vendored
18
.github/workflows/rust.yml
vendored
|
|
@ -18,7 +18,7 @@ concurrency:
|
|||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Build gitcomet-app in headless mode (no GPUI system deps required).
|
||||
# Build gitcomet in headless mode (no GPUI system deps required).
|
||||
# The UI-only code paths are behind #[cfg(feature = "ui-gpui")] guards.
|
||||
APP_FEATURES: "--no-default-features --features gix"
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ jobs:
|
|||
echo "Skipping cargo clippy -p gitcomet-ui-gpui in this job."
|
||||
echo "Reason: GPUI requires native windowing/system dependencies that are not available in headless CI."
|
||||
- name: Clippy (app — headless)
|
||||
run: cargo clippy -p gitcomet-app $APP_FEATURES -- -D warnings
|
||||
run: cargo clippy -p gitcomet $APP_FEATURES -- -D warnings
|
||||
|
||||
build:
|
||||
name: Build
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
- name: Build (core + state + backend)
|
||||
run: cargo build -p gitcomet-core -p gitcomet-state -p gitcomet-git-gix --verbose
|
||||
- name: Build (app — headless)
|
||||
run: cargo build -p gitcomet-app $APP_FEATURES --verbose
|
||||
run: cargo build -p gitcomet $APP_FEATURES --verbose
|
||||
|
||||
# Core merge algorithm correctness — Phase 1A/1B/1C portability tests
|
||||
merge-algorithm:
|
||||
|
|
@ -148,16 +148,16 @@ jobs:
|
|||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache Rust artifacts
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Build gitcomet-app binary (headless)
|
||||
run: cargo build -p gitcomet-app $APP_FEATURES
|
||||
- name: Build gitcomet binary (headless)
|
||||
run: cargo build -p gitcomet $APP_FEATURES
|
||||
- name: Git mergetool E2E (Phase 4A — t7610 parity)
|
||||
run: cargo test -p gitcomet-app $APP_FEATURES --test mergetool_git_integration --verbose
|
||||
run: cargo test -p gitcomet $APP_FEATURES --test mergetool_git_integration --verbose
|
||||
- name: Git difftool E2E (Phase 4B — t7800 parity)
|
||||
run: cargo test -p gitcomet-app $APP_FEATURES --test difftool_git_integration --verbose
|
||||
run: cargo test -p gitcomet $APP_FEATURES --test difftool_git_integration --verbose
|
||||
- name: Standalone tool-mode E2E (exit codes + validation)
|
||||
run: cargo test -p gitcomet-app $APP_FEATURES --test standalone_tool_mode_integration --verbose
|
||||
run: cargo test -p gitcomet $APP_FEATURES --test standalone_tool_mode_integration --verbose
|
||||
- name: Mergetool/difftool runtime unit tests (bin target)
|
||||
run: cargo test -p gitcomet-app $APP_FEATURES --bin gitcomet-app --verbose
|
||||
run: cargo test -p gitcomet $APP_FEATURES --bin gitcomet --verbose
|
||||
|
||||
# Backend integration — mergetool launcher, status, conflict checkout
|
||||
backend-integration:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
- `crates/gitcomet-state`: MVU state store, reducers, effects, conflict session management.
|
||||
- `crates/gitcomet-ui`: UI model/state (toolkit-independent).
|
||||
- `crates/gitcomet-ui-gpui`: gpui views/components (focused diff/merge windows, conflict resolver, word diff).
|
||||
- `crates/gitcomet-app`: binary entrypoint, CLI (clap), difftool/mergetool/setup/uninstall modes.
|
||||
- `crates/gitcomet`: binary entrypoint, CLI (clap), difftool/mergetool/setup/uninstall modes.
|
||||
|
||||
### Getting started
|
||||
|
||||
|
|
@ -28,19 +28,19 @@ cargo build
|
|||
To build the actual app you'll enable features (requires network for dependencies):
|
||||
|
||||
```bash
|
||||
cargo build -p gitcomet-app --features ui,gix
|
||||
cargo build -p gitcomet --features ui,gix
|
||||
```
|
||||
|
||||
To also compile the gpui-based UI crate:
|
||||
|
||||
```bash
|
||||
cargo build -p gitcomet-app --features ui-gpui,gix
|
||||
cargo build -p gitcomet --features ui-gpui,gix
|
||||
```
|
||||
|
||||
Run (opens the repo passed as the first arg, or falls back to the current directory):
|
||||
|
||||
```bash
|
||||
cargo run -p gitcomet-app --features ui-gpui,gix -- /path/to/repo
|
||||
cargo run -p gitcomet --features ui-gpui,gix -- /path/to/repo
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
|
@ -86,11 +86,12 @@ The release workflow `.github/workflows/build-release-artifacts.yml` builds and
|
|||
- Windows: portable ZIP + MSI
|
||||
- Linux: tar.gz + AppImage + .deb
|
||||
- macOS: DMG + tar.gz for `arm64` and `x86_64`
|
||||
- Homebrew formula asset: `gitcomet.rb` (generated from macOS + Linux x86_64 tarballs and their SHA256 values)
|
||||
- Homebrew cask asset: `gitcomet.rb` (generated from macOS DMG artifacts and their SHA256 values)
|
||||
- Homebrew CLI formula asset: `gitcomet-cli.rb` (generated from macOS + Linux x86_64 tarballs and their SHA256 values)
|
||||
|
||||
### Homebrew deployment
|
||||
|
||||
To push `Formula/gitcomet.rb` into a Homebrew tap repo automatically on release:
|
||||
To push `Casks/gitcomet.rb` and `Formula/gitcomet-cli.rb` into a Homebrew tap repo automatically on release:
|
||||
|
||||
1. Create a tap repository (default expected name: `OWNER/homebrew-gitcomet`).
|
||||
2. In this repo, configure:
|
||||
|
|
@ -103,6 +104,27 @@ This release flow will:
|
|||
|
||||
- build and upload release artifacts
|
||||
- publish the GitHub release
|
||||
- call `.github/workflows/deploy-homebrew-tap.yml` to update `Formula/gitcomet.rb` in the tap repo
|
||||
- call `.github/workflows/deploy-homebrew-tap.yml` to update `Casks/gitcomet.rb` and `Formula/gitcomet-cli.rb` in the tap repo
|
||||
|
||||
You can also run `.github/workflows/deploy-homebrew-tap.yml` manually for backfills or dry-runs.
|
||||
|
||||
### AUR mirror deployment
|
||||
|
||||
To push `PKGBUILD` and `.SRCINFO` into a GitHub-hosted AUR mirror repo automatically on release:
|
||||
|
||||
1. Create the target repository (default expected name: `OWNER/aur-gitcomet`).
|
||||
2. In this repo, configure:
|
||||
- secret `AUR_REPO_TOKEN`: GitHub token with `contents:write` access to the AUR mirror repository.
|
||||
- optional variable `AUR_GITHUB_REPO`: target repository in `OWNER/REPO` form.
|
||||
- optional variable `AUR_GITHUB_BRANCH`: target branch (default `main`).
|
||||
3. Run `.github/workflows/release-manual-main.yml` with `draft=false`.
|
||||
|
||||
This release flow will:
|
||||
|
||||
- download the published Linux release tarball and source tarball
|
||||
- update `PKGBUILD` `pkgver` and `sha256sums`
|
||||
- regenerate `.SRCINFO`
|
||||
- validate sources with `makepkg --verifysource`
|
||||
- push the updated metadata into the configured AUR mirror repository
|
||||
|
||||
You can also run `.github/workflows/deploy-aur.yml` manually for backfills or dry-runs.
|
||||
|
|
|
|||
71
Cargo.lock
generated
71
Cargo.lock
generated
|
|
@ -2239,8 +2239,8 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitcomet-app"
|
||||
version = "0.1.0"
|
||||
name = "gitcomet"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"embed-resource",
|
||||
|
|
@ -2256,7 +2256,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gitcomet-core"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"rustc-hash 2.1.1",
|
||||
|
|
@ -2267,14 +2267,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gitcomet-git"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"gitcomet-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitcomet-git-gix"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"gitcomet-core",
|
||||
"gix",
|
||||
|
|
@ -2285,7 +2285,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gitcomet-state"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"gitcomet-core",
|
||||
"gix",
|
||||
|
|
@ -2299,14 +2299,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gitcomet-ui"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"gitcomet-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitcomet-ui-gpui"
|
||||
version = "0.1.0"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"gitcomet-core",
|
||||
|
|
@ -2399,7 +2399,7 @@ dependencies = [
|
|||
"bstr",
|
||||
"gix-date",
|
||||
"gix-error",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2501,7 +2501,7 @@ dependencies = [
|
|||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-bom",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2760,7 +2760,7 @@ dependencies = [
|
|||
"itoa",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2859,7 +2859,7 @@ dependencies = [
|
|||
"maybe-async",
|
||||
"nonempty",
|
||||
"thiserror 2.0.18",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2891,7 +2891,7 @@ dependencies = [
|
|||
"gix-validate",
|
||||
"memmap2",
|
||||
"thiserror 2.0.18",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5314,7 +5314,7 @@ version = "3.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.25.4+spec-1.1.0",
|
||||
"toml_edit 0.25.5+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7044,7 +7044,7 @@ dependencies = [
|
|||
"toml_datetime 0.7.5+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7067,9 +7067,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.0.0+spec-1.1.0"
|
||||
version = "1.0.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
|
@ -7085,28 +7085,28 @@ dependencies = [
|
|||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.4+spec-1.1.0"
|
||||
version = "0.25.5+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
||||
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.0.0+spec-1.1.0",
|
||||
"toml_datetime 1.0.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.9+spec-1.1.0"
|
||||
version = "1.0.10+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7117,9 +7117,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.0.7+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
|
|
@ -8426,6 +8426,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.55.0"
|
||||
|
|
@ -8735,7 +8744,7 @@ dependencies = [
|
|||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
|
|
@ -8763,7 +8772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
|
|
@ -9039,7 +9048,7 @@ dependencies = [
|
|||
"enumflags2",
|
||||
"serde",
|
||||
"url",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
|
@ -9067,5 +9076,5 @@ dependencies = [
|
|||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.117",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"crates/gitcomet-app",
|
||||
"crates/gitcomet",
|
||||
"crates/gitcomet-core",
|
||||
"crates/gitcomet-git",
|
||||
"crates/gitcomet-git-gix",
|
||||
|
|
@ -11,7 +11,7 @@ members = [
|
|||
]
|
||||
|
||||
default-members = [
|
||||
"crates/gitcomet-app",
|
||||
"crates/gitcomet",
|
||||
"crates/gitcomet-core",
|
||||
"crates/gitcomet-git",
|
||||
"crates/gitcomet-git-gix",
|
||||
|
|
@ -21,6 +21,7 @@ default-members = [
|
|||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.7"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-only"
|
||||
authors = ["AutoExplore Oy <info@autoexplore.ai>"]
|
||||
|
|
@ -82,7 +83,7 @@ opt-level = 1
|
|||
split-debuginfo = "packed"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
strip = "symbols"
|
||||
|
||||
|
|
|
|||
42
README.md
42
README.md
|
|
@ -4,8 +4,11 @@
|
|||
[](https://discord.gg/2ufDGP8RnA)
|
||||
[](https://gitcomet.dev)
|
||||
[](https://autoexplore.ai)
|
||||
[](LICENSE)
|
||||
[](https://github.com/Auto-Explore/gitcomet/releases/latest)
|
||||
[](https://github.com/Auto-Explore/gitcomet/releases)
|
||||
|
||||
**Speed is a feature.**
|
||||
**Fastest Open Source Git GUI**
|
||||
|
||||
GitComet is built for teams that want fast Git operations with local-first privacy, familiar workflows, and open source freedom.
|
||||
|
||||
|
|
@ -19,11 +22,24 @@ Download the latest prebuilt binaries/installers from [GitHub Releases](https://
|
|||
|
||||
#### Homebrew
|
||||
|
||||
install from tap:
|
||||
install app from tap (recommended):
|
||||
|
||||
```bash
|
||||
brew tap auto-explore/gitcomet
|
||||
brew install gitcomet
|
||||
brew install --cask gitcomet
|
||||
```
|
||||
|
||||
|
||||
**MacOS Troubleshooting:** If you downloaded the app manually and macOS reports that GitComet *"is damaged and can't be opened"*, this is due to Apple's Gatekeeper quarantine for apps downloaded outside the App Store. To resolve this, run the following command in your terminal to remove the quarantine attribute:
|
||||
|
||||
```bash
|
||||
xattr -d com.apple.quarantine /Applications/GitComet.app
|
||||
```
|
||||
|
||||
optional CLI install:
|
||||
|
||||
```bash
|
||||
brew install gitcomet-cli
|
||||
```
|
||||
|
||||
### Fast, Free, Familiar
|
||||
|
|
@ -71,13 +87,13 @@ Measured on Linux 6.19.6-zen (x64), Ryzen 5950x, 128GB DDR4. Detailed test steps
|
|||
- Code test coverage workflows
|
||||
- GitHub and Azure DevOps integrations
|
||||
- Priority improvements during early access
|
||||
- Join waitlist: [gitcomet.com/#pricing](https://gitcomet.com/#pricing)
|
||||
- Join waitlist: [gitcomet.dev/#pricing](https://gitcomet.dev/#pricing)
|
||||
|
||||
### Build from source
|
||||
|
||||
```bash
|
||||
cargo build -p gitcomet-app --features ui-gpui,gix
|
||||
cargo run -p gitcomet-app --features ui-gpui,gix -- /path/to/repo
|
||||
cargo build -p gitcomet --features ui-gpui,gix
|
||||
cargo run -p gitcomet --features ui-gpui,gix -- /path/to/repo
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
|
@ -92,10 +108,10 @@ GitComet can be used as a standalone diff and merge tool invoked by `git difftoo
|
|||
|
||||
```bash
|
||||
# Configure Git globally to use GitComet for both difftool + mergetool
|
||||
gitcomet-app setup
|
||||
gitcomet setup
|
||||
|
||||
# Remove GitComet integration safely
|
||||
gitcomet-app uninstall
|
||||
gitcomet uninstall
|
||||
```
|
||||
|
||||
- Use `--local` to target only the current repository instead of global config.
|
||||
|
|
@ -110,7 +126,7 @@ This setup registers both headless and GUI variants with `guiDefault=auto`, so G
|
|||
Built-in `setup` writes these Git config entries:
|
||||
|
||||
```bash
|
||||
GITCOMET_BIN="/absolute/path/to/gitcomet-app"
|
||||
GITCOMET_BIN="/absolute/path/to/gitcomet"
|
||||
|
||||
# Headless tool: algorithm-only merge/diff for CI, scripts, and no-display environments
|
||||
git config --global merge.tool gitcomet
|
||||
|
|
@ -153,7 +169,7 @@ Built-in `uninstall` restores those backups only when the key still has the setu
|
|||
**Difftool:**
|
||||
|
||||
```bash
|
||||
gitcomet-app difftool --local <path> --remote <path> [--path <display_name>] [--label-left <label>] [--label-right <label>]
|
||||
gitcomet 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.
|
||||
|
|
@ -161,7 +177,7 @@ Also reads `LOCAL`/`REMOTE` from environment as a fallback when invoked by Git.
|
|||
**Mergetool:**
|
||||
|
||||
```bash
|
||||
gitcomet-app mergetool --local <path> --remote <path> --merged <path> [--base <path>] [--label-local <label>] [--label-remote <label>] [--label-base <label>]
|
||||
gitcomet 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.
|
||||
|
|
@ -188,6 +204,10 @@ 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
|
||||
|
||||
[](https://star-history.com/#Auto-Explore/gitcomet&Date)
|
||||
|
||||
### License
|
||||
|
||||
GitComet is licensed under the GNU Affero General Public License Version 3
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Type=Application
|
||||
Name=GitComet
|
||||
Comment=Git UI built with GPUI
|
||||
Exec=gitcomet-app
|
||||
Exec=gitcomet
|
||||
Icon=gitcomet
|
||||
StartupWMClass=gitcomet
|
||||
Terminal=false
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gitcomet-core"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gitcomet-git-gix"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
use gitcomet_core::domain::{Remote, RemoteBranch, Upstream};
|
||||
use gitcomet_core::error::{Error, ErrorKind};
|
||||
use gitcomet_core::services::{CommandOutput, PullMode, RemoteUrlKind, Result};
|
||||
use gix::bstr::ByteSlice as _;
|
||||
|
|
@ -147,22 +147,38 @@ impl GixRepo {
|
|||
Ok(Some(head.to_string()))
|
||||
}
|
||||
|
||||
fn branch_has_upstream(&self, branch: &str) -> Result<bool> {
|
||||
validate_ref_like_arg(branch, "branch name")?;
|
||||
fn branch_upstream(&self, branch_name: &str) -> Result<Option<Upstream>> {
|
||||
validate_ref_like_arg(branch_name, "branch name")?;
|
||||
|
||||
let repo = self.reopen_repo()?;
|
||||
let ref_name = format!("refs/heads/{branch}");
|
||||
let ref_name = format!("refs/heads/{branch_name}");
|
||||
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(false);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(matches!(
|
||||
reference.remote_tracking_ref_name(gix::remote::Direction::Fetch),
|
||||
Some(Ok(_))
|
||||
))
|
||||
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())
|
||||
}
|
||||
|
||||
pub(super) fn list_remotes_impl(&self) -> Result<Vec<Remote>> {
|
||||
|
|
@ -350,16 +366,51 @@ 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()?
|
||||
&& !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,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut cmd = self.git_workdir_cmd();
|
||||
|
|
@ -376,6 +427,17 @@ 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)
|
||||
|
|
|
|||
|
|
@ -201,8 +201,7 @@ fn require_git_local_push_for_remote_management_tests() -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
fn init_repo_with_user(repo: &Path) {
|
||||
run_git(repo, &["init"]);
|
||||
fn configure_repo_with_user(repo: &Path) {
|
||||
run_git(repo, &["config", "user.email", "you@example.com"]);
|
||||
run_git(repo, &["config", "user.name", "You"]);
|
||||
run_git(repo, &["config", "commit.gpgsign", "false"]);
|
||||
|
|
@ -210,6 +209,11 @@ fn init_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();
|
||||
|
|
@ -345,6 +349,64 @@ 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();
|
||||
|
|
@ -508,7 +570,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"]);
|
||||
init_repo_with_user(&work_repo);
|
||||
configure_repo_with_user(&work_repo);
|
||||
|
||||
let remote_str = git_remote_url(&remote_repo);
|
||||
run_git(&work_repo, &["remote", "add", "origin", &remote_str]);
|
||||
|
|
@ -556,6 +618,76 @@ 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();
|
||||
|
|
@ -574,7 +706,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"]);
|
||||
init_repo_with_user(&repo_a);
|
||||
configure_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(
|
||||
|
|
@ -593,7 +725,7 @@ fn pull_non_output_supports_all_modes_when_upstream_exists() {
|
|||
repo_b.to_string_lossy().as_ref(),
|
||||
],
|
||||
);
|
||||
init_repo_with_user(&repo_b);
|
||||
configure_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"]);
|
||||
|
|
@ -723,7 +855,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"]);
|
||||
init_repo_with_user(&work_repo);
|
||||
configure_repo_with_user(&work_repo);
|
||||
|
||||
let remote_str = git_remote_url(&remote_repo);
|
||||
run_git(&work_repo, &["remote", "add", "origin", &remote_str]);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gitcomet-git"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gitcomet-state"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -684,7 +684,7 @@ mod tests {
|
|||
"/tmp/target/debug/gitcomet_ui_gpui-3ad1b0fd3f0c0d3e"
|
||||
)));
|
||||
assert!(!looks_like_test_binary(Path::new(
|
||||
"/tmp/target/debug/gitcomet-app"
|
||||
"/tmp/target/debug/gitcomet"
|
||||
)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gitcomet-ui-gpui"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Focused diff window for standalone `gitcomet-app difftool` invocation.
|
||||
//! Focused diff window for standalone `gitcomet 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).
|
||||
|
|
|
|||
|
|
@ -1601,6 +1601,11 @@ 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)
|
||||
|
|
|
|||
|
|
@ -293,9 +293,15 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -397,13 +403,17 @@ 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: 0..0,
|
||||
source_line_range,
|
||||
change_hint: MarkdownChangeHint::None,
|
||||
indent_level: 0,
|
||||
blockquote_level: 0,
|
||||
|
|
@ -1207,9 +1217,57 @@ 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,
|
||||
|
|
@ -1928,6 +1986,61 @@ 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]
|
||||
|
|
@ -2665,6 +2778,29 @@ 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) =
|
||||
|
|
@ -3091,17 +3227,18 @@ code line
|
|||
";
|
||||
let doc = parse(src);
|
||||
|
||||
// Should have: Heading, Paragraph, ListItem, ListItem, CodeLine, ThematicBreak
|
||||
// Should have: Heading, Spacer, Paragraph, ListItem, ListItem, CodeLine, ThematicBreak
|
||||
assert!(
|
||||
doc.rows.len() >= 6,
|
||||
"expected at least 6 rows, got {}",
|
||||
doc.rows.len() >= 7,
|
||||
"expected at least 7 rows, got {}",
|
||||
doc.rows.len()
|
||||
);
|
||||
assert!(matches!(
|
||||
doc.rows[0].kind,
|
||||
MarkdownPreviewRowKind::Heading { level: 1 }
|
||||
));
|
||||
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Paragraph);
|
||||
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
|
||||
assert_eq!(doc.rows[2].kind, MarkdownPreviewRowKind::Paragraph);
|
||||
}
|
||||
|
||||
// ── Internal helpers ────────────────────────────────────────────────
|
||||
|
|
@ -3163,25 +3300,27 @@ 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(), 2);
|
||||
assert_eq!(doc.rows.len(), 3);
|
||||
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].text.as_ref(), "Visible");
|
||||
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
|
||||
assert_eq!(doc.rows[2].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(), 2);
|
||||
assert_eq!(doc.rows.len(), 3);
|
||||
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].text.as_ref(), "Visible");
|
||||
assert_eq!(doc.rows[1].kind, MarkdownPreviewRowKind::Spacer);
|
||||
assert_eq!(doc.rows[2].text.as_ref(), "Visible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1247,20 +1247,33 @@ fn whole_file_conflict_streamed_three_way_syntax_survives_view_mode_switch(
|
|||
let _ = window.draw(app);
|
||||
});
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
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| {
|
||||
view.update(app, |this, cx| {
|
||||
|
|
@ -1276,18 +1289,28 @@ fn whole_file_conflict_streamed_three_way_syntax_survives_view_mode_switch(
|
|||
let _ = window.draw(app);
|
||||
});
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
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(),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
fixture.cleanup();
|
||||
}
|
||||
|
|
@ -3564,26 +3587,27 @@ 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);
|
||||
|
|
@ -3600,10 +3624,6 @@ 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");
|
||||
|
|
@ -3612,10 +3632,6 @@ 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");
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ 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()));
|
||||
|
|
@ -104,32 +105,40 @@ fn file_preview_renders_scrollable_syntax_highlighted_rows(cx: &mut gpui::TestAp
|
|||
let _ = window.draw(app);
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
});
|
||||
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,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&workdir);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,6 +324,333 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1045,8 +1045,6 @@ 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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -154,32 +154,36 @@ fn render_conflict_markdown_preview_rows(
|
|||
return Vec::new();
|
||||
};
|
||||
let document = Arc::clone(document);
|
||||
let (row_id_prefix, horizontal_scroll_handle) = match side {
|
||||
ThreeWayColumn::Base => (
|
||||
"conflict_markdown_preview_base",
|
||||
let viewport_width = match side {
|
||||
ThreeWayColumn::Base => {
|
||||
this.conflict_resolver_diff_scroll
|
||||
.0
|
||||
.borrow()
|
||||
.base_handle
|
||||
.clone(),
|
||||
),
|
||||
ThreeWayColumn::Ours => (
|
||||
"conflict_markdown_preview_ours",
|
||||
.bounds()
|
||||
.size
|
||||
.width
|
||||
}
|
||||
ThreeWayColumn::Ours => {
|
||||
this.conflict_preview_ours_scroll
|
||||
.0
|
||||
.borrow()
|
||||
.base_handle
|
||||
.clone(),
|
||||
),
|
||||
ThreeWayColumn::Theirs => (
|
||||
"conflict_markdown_preview_theirs",
|
||||
.bounds()
|
||||
.size
|
||||
.width
|
||||
}
|
||||
ThreeWayColumn::Theirs => {
|
||||
this.conflict_preview_theirs_scroll
|
||||
.0
|
||||
.borrow()
|
||||
.base_handle
|
||||
.clone(),
|
||||
),
|
||||
};
|
||||
.bounds()
|
||||
.size
|
||||
.width
|
||||
}
|
||||
}
|
||||
.max(px(0.0));
|
||||
this.update_markdown_preview_horizontal_min_width(
|
||||
document.as_ref(),
|
||||
range.clone(),
|
||||
|
|
@ -193,9 +197,7 @@ fn render_conflict_markdown_preview_rows(
|
|||
&super::history::MarkdownPreviewRenderContext {
|
||||
theme,
|
||||
bar_color: None,
|
||||
min_width: this.diff_horizontal_min_width,
|
||||
row_id_prefix,
|
||||
horizontal_scroll_handle: Some(horizontal_scroll_handle),
|
||||
min_width: this.diff_horizontal_min_width.max(viewport_width),
|
||||
view: None,
|
||||
text_region: DiffTextRegion::Inline,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
; 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
|
||||
|
|
@ -1,27 +1,17 @@
|
|||
; Vendored from Zed (zed/extensions/html/languages/html/highlights.scm)
|
||||
; Derived from
|
||||
; gpui-component/crates/ui/src/highlighter/languages/html/highlights.scm
|
||||
; (Apache-2.0).
|
||||
|
||||
(tag_name) @tag
|
||||
|
||||
(doctype) @tag.doctype
|
||||
|
||||
(erroneous_end_tag_name) @tag.error
|
||||
(doctype) @constant
|
||||
(attribute_name) @attribute
|
||||
|
||||
[
|
||||
"\""
|
||||
"'"
|
||||
(attribute_value)
|
||||
] @string
|
||||
|
||||
(attribute_value) @string
|
||||
(comment) @comment
|
||||
|
||||
(entity) @string.special
|
||||
|
||||
"=" @punctuation.delimiter
|
||||
|
||||
[
|
||||
"<"
|
||||
">"
|
||||
"<!"
|
||||
"</"
|
||||
"/>"
|
||||
] @punctuation.bracket
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
; Vendored from Zed (zed/extensions/html/languages/html/injections.scm)
|
||||
; Derived from
|
||||
; gpui-component/crates/ui/src/highlighter/languages/html/injections.scm
|
||||
; (Apache-2.0). Local additions preserve inline `style=` and `on*=` injections.
|
||||
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
||||
((script_element
|
||||
(raw_text) @injection.content)
|
||||
(#set! injection.language "javascript"))
|
||||
|
||||
(script_element
|
||||
(raw_text) @injection.content
|
||||
(#set! injection.language "javascript"))
|
||||
|
||||
(style_element
|
||||
(raw_text) @injection.content
|
||||
(#set! injection.language "css"))
|
||||
((style_element
|
||||
(raw_text) @injection.content)
|
||||
(#set! injection.language "css"))
|
||||
|
||||
(attribute
|
||||
(attribute_name) @_attribute_name
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
; 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.
|
||||
; 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.
|
||||
|
||||
; 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)
|
||||
|
||||
|
|
@ -44,6 +48,8 @@
|
|||
right: [(function_expression) (arrow_function)])
|
||||
|
||||
; Function and method calls
|
||||
;--------------------------
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @function)
|
||||
|
||||
|
|
@ -55,6 +61,8 @@
|
|||
constructor: (identifier) @type)
|
||||
|
||||
; Parameters
|
||||
;-----------
|
||||
|
||||
(arrow_function
|
||||
parameter: (identifier) @variable.parameter)
|
||||
|
||||
|
|
@ -62,31 +70,43 @@
|
|||
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) @constructor
|
||||
(#match? @constructor "^[A-Z]"))
|
||||
((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
|
||||
|
||||
(super) @variable.special
|
||||
|
||||
[
|
||||
(null)
|
||||
(undefined)
|
||||
] @constant.builtin
|
||||
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
] @boolean
|
||||
|
||||
[
|
||||
(null)
|
||||
(undefined)
|
||||
] @constant.builtin
|
||||
|
||||
(comment) @comment
|
||||
|
||||
(hash_bang_line) @comment
|
||||
|
|
@ -98,11 +118,13 @@
|
|||
|
||||
(escape_sequence) @string.escape
|
||||
|
||||
(regex) @string.regex
|
||||
(regex) @string.special
|
||||
|
||||
(number) @number
|
||||
|
||||
; Tokens
|
||||
;-------
|
||||
|
||||
[
|
||||
";"
|
||||
(optional_chain)
|
||||
|
|
@ -159,9 +181,6 @@
|
|||
"..."
|
||||
] @operator
|
||||
|
||||
(regex
|
||||
"/" @string.regex)
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
|
|
@ -177,62 +196,49 @@
|
|||
":"
|
||||
] @operator)
|
||||
|
||||
; Keywords — split into declaration / import / control for richer styling
|
||||
[
|
||||
"as"
|
||||
"async"
|
||||
"await"
|
||||
"debugger"
|
||||
"default"
|
||||
"delete"
|
||||
"extends"
|
||||
"get"
|
||||
"in"
|
||||
"instanceof"
|
||||
"new"
|
||||
"of"
|
||||
"set"
|
||||
"static"
|
||||
"target"
|
||||
"typeof"
|
||||
"void"
|
||||
"with"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
"const"
|
||||
"let"
|
||||
"var"
|
||||
"function"
|
||||
"class"
|
||||
] @keyword.declaration
|
||||
|
||||
[
|
||||
"export"
|
||||
"from"
|
||||
"import"
|
||||
] @keyword.import
|
||||
|
||||
[
|
||||
"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"
|
||||
"throw"
|
||||
"try"
|
||||
"typeof"
|
||||
"var"
|
||||
"void"
|
||||
"while"
|
||||
"with"
|
||||
"yield"
|
||||
] @keyword.control
|
||||
|
||||
(switch_default
|
||||
"default" @keyword.control)
|
||||
] @keyword
|
||||
|
||||
(template_substitution
|
||||
"${" @punctuation.special
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
; Vendored from Zed (zed/crates/languages/src/rust/highlights.scm)
|
||||
; Provides richer capture semantics than tree-sitter-rust crate defaults.
|
||||
; Derived from
|
||||
; gpui-component/crates/ui/src/highlighter/languages/rust/highlights.scm
|
||||
; (Apache-2.0). Local additions preserve GitComet's richer diff token classes.
|
||||
|
||||
(identifier) @variable
|
||||
|
||||
|
|
@ -32,6 +33,39 @@
|
|||
(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
|
||||
|
|
@ -50,12 +84,14 @@
|
|||
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
|
||||
|
|
@ -69,18 +105,17 @@
|
|||
(macro_definition
|
||||
name: (identifier) @function.special.definition)
|
||||
|
||||
; Identifier conventions
|
||||
; Assume uppercase names are types/enum-constructors
|
||||
((identifier) @type
|
||||
(#match? @type "^[A-Z]"))
|
||||
[
|
||||
(line_comment)
|
||||
(block_comment)
|
||||
] @comment
|
||||
|
||||
; 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)
|
||||
[
|
||||
(line_comment
|
||||
(doc_comment))
|
||||
(block_comment
|
||||
(doc_comment))
|
||||
] @comment.doc
|
||||
|
||||
[
|
||||
"("
|
||||
|
|
@ -91,8 +126,11 @@
|
|||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
(_
|
||||
.
|
||||
(type_arguments
|
||||
"<" @punctuation.bracket
|
||||
">" @punctuation.bracket)
|
||||
|
||||
(type_parameters
|
||||
"<" @punctuation.bracket
|
||||
">" @punctuation.bracket)
|
||||
|
||||
|
|
@ -168,18 +206,6 @@
|
|||
|
||||
(boolean_literal) @boolean
|
||||
|
||||
[
|
||||
(line_comment)
|
||||
(block_comment)
|
||||
] @comment
|
||||
|
||||
[
|
||||
(line_comment
|
||||
(doc_comment))
|
||||
(block_comment
|
||||
(doc_comment))
|
||||
] @comment.doc
|
||||
|
||||
[
|
||||
"!="
|
||||
"%"
|
||||
|
|
@ -219,7 +245,6 @@
|
|||
"?"
|
||||
] @operator
|
||||
|
||||
; Avoid highlighting these as operators when used in doc comments.
|
||||
(unary_expression
|
||||
"!" @operator)
|
||||
|
||||
|
|
@ -232,32 +257,6 @@ operator: "/" @operator
|
|||
(parameter
|
||||
(identifier) @variable.parameter)
|
||||
|
||||
(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_]*$"))
|
||||
]))
|
||||
(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_]*$"))
|
||||
]))
|
||||
(inner_attribute_item) @attribute
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
; 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))
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
; 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
|
||||
|
||||
|
|
@ -156,6 +161,14 @@
|
|||
] @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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
; 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
|
||||
|
||||
|
|
@ -102,6 +107,14 @@
|
|||
] @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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = include_str!("queries/css_highlights.scm");
|
||||
const CSS_HIGHLIGHTS_QUERY: &str = tree_sitter_css::HIGHLIGHTS_QUERY;
|
||||
#[cfg(feature = "syntax-web")]
|
||||
const JAVASCRIPT_HIGHLIGHTS_QUERY: &str = include_str!("queries/javascript_highlights.scm");
|
||||
#[cfg(feature = "syntax-web")]
|
||||
|
|
@ -34,6 +34,8 @@ 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;
|
||||
|
||||
|
|
@ -1929,7 +1931,6 @@ impl TreesitterQueryAsset {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntax-web")]
|
||||
const fn with_injections(highlights: &'static str, injections: &'static str) -> Self {
|
||||
Self {
|
||||
highlights,
|
||||
|
|
@ -2734,7 +2735,7 @@ fn tree_sitter_grammar(
|
|||
#[cfg(feature = "syntax-rust")]
|
||||
DiffSyntaxLanguage::Rust => Some((
|
||||
tree_sitter_rust::LANGUAGE.into(),
|
||||
TreesitterQueryAsset::highlights(RUST_HIGHLIGHTS_QUERY),
|
||||
TreesitterQueryAsset::with_injections(RUST_HIGHLIGHTS_QUERY, RUST_INJECTIONS_QUERY),
|
||||
)),
|
||||
#[cfg(feature = "syntax-python")]
|
||||
DiffSyntaxLanguage::Python => Some((
|
||||
|
|
|
|||
|
|
@ -105,7 +105,15 @@ impl MainPaneView {
|
|||
};
|
||||
let document = Arc::clone(document);
|
||||
let bar_color = worktree_preview_bar_color(this, theme);
|
||||
let horizontal_scroll_handle = this.worktree_preview_scroll.0.borrow().base_handle.clone();
|
||||
let viewport_width = this
|
||||
.worktree_preview_scroll
|
||||
.0
|
||||
.borrow()
|
||||
.base_handle
|
||||
.bounds()
|
||||
.size
|
||||
.width
|
||||
.max(px(0.0));
|
||||
this.update_markdown_preview_horizontal_min_width(
|
||||
document.as_ref(),
|
||||
range.clone(),
|
||||
|
|
@ -119,9 +127,7 @@ impl MainPaneView {
|
|||
&MarkdownPreviewRenderContext {
|
||||
theme,
|
||||
bar_color,
|
||||
min_width: this.diff_horizontal_min_width,
|
||||
row_id_prefix: "worktree_markdown_preview",
|
||||
horizontal_scroll_handle: Some(horizontal_scroll_handle),
|
||||
min_width: this.diff_horizontal_min_width.max(viewport_width),
|
||||
view: Some(cx.entity().clone()),
|
||||
text_region: DiffTextRegion::Inline,
|
||||
},
|
||||
|
|
@ -139,7 +145,15 @@ impl MainPaneView {
|
|||
return Vec::new();
|
||||
};
|
||||
let preview = Arc::clone(preview);
|
||||
let horizontal_scroll_handle = this.diff_scroll.0.borrow().base_handle.clone();
|
||||
let viewport_width = this
|
||||
.diff_scroll
|
||||
.0
|
||||
.borrow()
|
||||
.base_handle
|
||||
.bounds()
|
||||
.size
|
||||
.width
|
||||
.max(px(0.0));
|
||||
this.update_markdown_preview_horizontal_min_width(
|
||||
&preview.old,
|
||||
range.clone(),
|
||||
|
|
@ -157,9 +171,7 @@ impl MainPaneView {
|
|||
&MarkdownPreviewRenderContext {
|
||||
theme,
|
||||
bar_color: None,
|
||||
min_width: this.diff_horizontal_min_width,
|
||||
row_id_prefix: "diff_markdown_preview_left",
|
||||
horizontal_scroll_handle: Some(horizontal_scroll_handle),
|
||||
min_width: this.diff_horizontal_min_width.max(viewport_width),
|
||||
view: Some(cx.entity().clone()),
|
||||
text_region: region,
|
||||
},
|
||||
|
|
@ -177,7 +189,15 @@ impl MainPaneView {
|
|||
return Vec::new();
|
||||
};
|
||||
let preview = Arc::clone(preview);
|
||||
let horizontal_scroll_handle = this.diff_scroll.0.borrow().base_handle.clone();
|
||||
let viewport_width = this
|
||||
.diff_scroll
|
||||
.0
|
||||
.borrow()
|
||||
.base_handle
|
||||
.bounds()
|
||||
.size
|
||||
.width
|
||||
.max(px(0.0));
|
||||
this.update_markdown_preview_horizontal_min_width(
|
||||
&preview.inline,
|
||||
range.clone(),
|
||||
|
|
@ -191,9 +211,7 @@ impl MainPaneView {
|
|||
&MarkdownPreviewRenderContext {
|
||||
theme,
|
||||
bar_color: None,
|
||||
min_width: this.diff_horizontal_min_width,
|
||||
row_id_prefix: "diff_markdown_preview_inline",
|
||||
horizontal_scroll_handle: Some(horizontal_scroll_handle),
|
||||
min_width: this.diff_horizontal_min_width.max(viewport_width),
|
||||
view: Some(cx.entity().clone()),
|
||||
text_region: DiffTextRegion::Inline,
|
||||
},
|
||||
|
|
@ -211,7 +229,15 @@ impl MainPaneView {
|
|||
return Vec::new();
|
||||
};
|
||||
let preview = Arc::clone(preview);
|
||||
let horizontal_scroll_handle = this.diff_split_right_scroll.0.borrow().base_handle.clone();
|
||||
let viewport_width = this
|
||||
.diff_split_right_scroll
|
||||
.0
|
||||
.borrow()
|
||||
.base_handle
|
||||
.bounds()
|
||||
.size
|
||||
.width
|
||||
.max(px(0.0));
|
||||
this.update_markdown_preview_horizontal_min_width(
|
||||
&preview.new,
|
||||
range.clone(),
|
||||
|
|
@ -225,9 +251,7 @@ impl MainPaneView {
|
|||
&MarkdownPreviewRenderContext {
|
||||
theme,
|
||||
bar_color: None,
|
||||
min_width: this.diff_horizontal_min_width,
|
||||
row_id_prefix: "diff_markdown_preview_right",
|
||||
horizontal_scroll_handle: Some(horizontal_scroll_handle),
|
||||
min_width: this.diff_horizontal_min_width.max(viewport_width),
|
||||
view: Some(cx.entity().clone()),
|
||||
text_region: DiffTextRegion::SplitRight,
|
||||
},
|
||||
|
|
@ -257,10 +281,11 @@ impl MainPaneView {
|
|||
}
|
||||
}
|
||||
|
||||
const MARKDOWN_PREVIEW_ROW_HEIGHT_PX: f32 = 44.0;
|
||||
const MARKDOWN_PREVIEW_ROW_HEIGHT_PX: f32 = 28.0;
|
||||
const MARKDOWN_PREVIEW_BASE_FONT_PX: f32 = 13.0;
|
||||
const MARKDOWN_PREVIEW_BASE_LINE_HEIGHT_PX: f32 = 22.0;
|
||||
const MARKDOWN_PREVIEW_BASE_LINE_HEIGHT_PX: f32 = 20.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;
|
||||
|
|
@ -273,7 +298,6 @@ 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,
|
||||
|
|
@ -287,7 +311,6 @@ struct MarkdownPreviewRowTypography {
|
|||
struct MarkdownPreviewRowLayout {
|
||||
top_inset_px: f32,
|
||||
bottom_inset_px: f32,
|
||||
shell_bottom_inset_px: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
|
@ -300,8 +323,6 @@ 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,
|
||||
}
|
||||
|
|
@ -334,7 +355,6 @@ 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) {
|
||||
|
|
@ -342,7 +362,7 @@ fn markdown_preview_row_element(
|
|||
.relative()
|
||||
.h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
|
||||
.min_h(px(MARKDOWN_PREVIEW_ROW_HEIGHT_PX))
|
||||
.w_full()
|
||||
.w(min_width)
|
||||
.min_w(min_width)
|
||||
.into_any_element();
|
||||
}
|
||||
|
|
@ -351,18 +371,23 @@ 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()
|
||||
.flex_1()
|
||||
.relative()
|
||||
.flex_grow()
|
||||
.min_w(px(0.0))
|
||||
.w_full()
|
||||
.h_full()
|
||||
.h(px(typography.line_height))
|
||||
.min_h(px(typography.line_height))
|
||||
.flex()
|
||||
.items_center()
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.text_size(px(typography.font_size))
|
||||
.line_height(px(typography.line_height))
|
||||
.text_color(typography.text_color);
|
||||
.text_color(typography.text_color)
|
||||
.debug_selector(|| format!("markdown_preview_text_box_{row_ix}"));
|
||||
|
||||
if let Some(font_weight) = typography.font_weight {
|
||||
content = content.font_weight(font_weight);
|
||||
|
|
@ -370,10 +395,26 @@ 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_1()
|
||||
.flex_grow()
|
||||
.min_w(px(0.0))
|
||||
.w_full()
|
||||
.h_full()
|
||||
|
|
@ -394,7 +435,7 @@ fn markdown_preview_row_element(
|
|||
};
|
||||
|
||||
let mut line = div()
|
||||
.flex_1()
|
||||
.flex_grow()
|
||||
.min_w(px(0.0))
|
||||
.w_full()
|
||||
.h_full()
|
||||
|
|
@ -440,7 +481,7 @@ fn markdown_preview_row_element(
|
|||
};
|
||||
|
||||
let mut content_shell = div()
|
||||
.flex_1()
|
||||
.flex_grow()
|
||||
.min_w(px(0.0))
|
||||
.w_full()
|
||||
.h_full()
|
||||
|
|
@ -467,7 +508,7 @@ fn markdown_preview_row_element(
|
|||
shell = shell.border_t_1();
|
||||
}
|
||||
if is_last {
|
||||
shell = shell.border_b_1().pb(px(row_layout.shell_bottom_inset_px));
|
||||
shell = shell.border_b_1();
|
||||
}
|
||||
shell
|
||||
}
|
||||
|
|
@ -499,21 +540,20 @@ fn markdown_preview_row_element(
|
|||
_ => content_shell,
|
||||
};
|
||||
content_shell = content_shell.child(body);
|
||||
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),
|
||||
);
|
||||
if matches!(row.kind, MarkdownPreviewRowKind::CodeLine { .. }) {
|
||||
content_shell =
|
||||
content_shell.debug_selector(|| format!("markdown_preview_code_shell_{row_ix}"));
|
||||
}
|
||||
|
||||
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_1()
|
||||
.flex_grow()
|
||||
.min_w(px(0.0))
|
||||
.w_full()
|
||||
.w(row_content_width)
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
|
|
@ -526,16 +566,15 @@ 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.
|
||||
div()
|
||||
let row_container = 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_full()
|
||||
.w(min_width)
|
||||
.flex()
|
||||
.items_center()
|
||||
.pt(px(row_layout.top_inset_px))
|
||||
|
|
@ -552,7 +591,6 @@ 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| {
|
||||
|
|
@ -589,20 +627,16 @@ fn markdown_preview_row_element(
|
|||
});
|
||||
}
|
||||
})
|
||||
.child(DiffTextSelectionOverlay {
|
||||
view,
|
||||
visible_ix: row_ix,
|
||||
region: text_region,
|
||||
text: row_text,
|
||||
})
|
||||
.into_any_element()
|
||||
.child(row_content);
|
||||
row_container.into_any_element()
|
||||
} else {
|
||||
// Non-interactive markdown preview row (benchmarks, conflict resolver).
|
||||
div()
|
||||
let row_container = div()
|
||||
.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_full()
|
||||
.w(min_width)
|
||||
.flex()
|
||||
.items_center()
|
||||
.pt(px(row_layout.top_inset_px))
|
||||
|
|
@ -619,8 +653,8 @@ fn markdown_preview_row_element(
|
|||
)
|
||||
})
|
||||
.min_w(min_width)
|
||||
.child(row_content)
|
||||
.into_any_element()
|
||||
.child(row_content);
|
||||
row_container.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -976,59 +1010,49 @@ 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: 4.0,
|
||||
bottom_inset_px: 8.0,
|
||||
shell_bottom_inset_px: 0.0,
|
||||
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,
|
||||
},
|
||||
MarkdownPreviewRowKind::Heading { .. } => MarkdownPreviewRowLayout {
|
||||
top_inset_px: 3.0,
|
||||
bottom_inset_px: 7.0,
|
||||
shell_bottom_inset_px: 0.0,
|
||||
},
|
||||
MarkdownPreviewRowKind::DetailsSummary => MarkdownPreviewRowLayout {
|
||||
top_inset_px: 2.0,
|
||||
bottom_inset_px: 6.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: 3.0,
|
||||
bottom_inset_px: 7.0,
|
||||
shell_bottom_inset_px: 0.0,
|
||||
top_inset_px: 2.0,
|
||||
bottom_inset_px: 6.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 { 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
|
||||
},
|
||||
top_inset_px: if is_first { 5.0 } else { 0.0 },
|
||||
bottom_inset_px: if is_last { 5.0 } 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1042,77 +1066,77 @@ fn markdown_preview_row_typography(
|
|||
match row.kind {
|
||||
MarkdownPreviewRowKind::Heading { level: 1 } => MarkdownPreviewRowTypography {
|
||||
font_size: 28.0,
|
||||
line_height: 32.0,
|
||||
line_height: 28.0,
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::Heading { level: 2 } => MarkdownPreviewRowTypography {
|
||||
font_size: 24.0,
|
||||
line_height: 28.0,
|
||||
line_height: 24.0,
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::Heading { level: 3 } => MarkdownPreviewRowTypography {
|
||||
font_size: 20.0,
|
||||
line_height: 24.0,
|
||||
line_height: 22.0,
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::Heading { level: 4 } => MarkdownPreviewRowTypography {
|
||||
font_size: 18.0,
|
||||
line_height: 22.0,
|
||||
line_height: 20.0,
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::Heading { level: 5 } => MarkdownPreviewRowTypography {
|
||||
font_size: 16.0,
|
||||
line_height: 20.0,
|
||||
line_height: 18.0,
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::Heading { level: 6 } => MarkdownPreviewRowTypography {
|
||||
font_size: 14.0,
|
||||
line_height: 18.0,
|
||||
line_height: 16.0,
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::DetailsSummary => MarkdownPreviewRowTypography {
|
||||
font_size: MARKDOWN_PREVIEW_BASE_FONT_PX,
|
||||
line_height: 32.0,
|
||||
line_height: 28.0,
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::ListItem { .. } => MarkdownPreviewRowTypography {
|
||||
font_size: MARKDOWN_PREVIEW_BASE_FONT_PX,
|
||||
line_height: 36.0,
|
||||
line_height: MARKDOWN_PREVIEW_BASE_LINE_HEIGHT_PX,
|
||||
font_weight: None,
|
||||
font_family: None,
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::CodeLine { .. } => MarkdownPreviewRowTypography {
|
||||
font_size: 12.0,
|
||||
line_height: 20.0,
|
||||
line_height: 18.0,
|
||||
font_weight: None,
|
||||
font_family: Some(UI_MONOSPACE_FONT_FAMILY),
|
||||
text_color,
|
||||
},
|
||||
MarkdownPreviewRowKind::TableRow { is_header } => MarkdownPreviewRowTypography {
|
||||
font_size: 12.0,
|
||||
line_height: 20.0,
|
||||
line_height: 18.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: 20.0,
|
||||
line_height: 18.0,
|
||||
font_weight: None,
|
||||
font_family: Some(UI_MONOSPACE_FONT_FAMILY),
|
||||
text_color,
|
||||
|
|
@ -1143,15 +1167,11 @@ 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 {
|
||||
left_px: default_left_px,
|
||||
right_px: 0.0,
|
||||
// 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,
|
||||
},
|
||||
_ => MarkdownPreviewRowHorizontalPadding {
|
||||
left_px: default_left_px,
|
||||
|
|
@ -1795,7 +1815,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_preview_list_rows_tighten_line_height_relative_to_paragraphs() {
|
||||
fn markdown_preview_list_rows_match_body_line_height_and_keep_tighter_layout() {
|
||||
let theme = AppTheme::zed_one_light();
|
||||
let paragraph = markdown_row(MarkdownPreviewRowKind::Paragraph);
|
||||
let list_item = markdown_row(MarkdownPreviewRowKind::ListItem { number: None });
|
||||
|
|
@ -1805,7 +1825,10 @@ mod tests {
|
|||
let paragraph_layout = markdown_preview_row_layout(¶graph);
|
||||
let list_layout = markdown_preview_row_layout(&list_item);
|
||||
|
||||
assert!(list_typography.line_height > paragraph_typography.line_height);
|
||||
assert_eq!(
|
||||
list_typography.line_height,
|
||||
paragraph_typography.line_height
|
||||
);
|
||||
assert!(paragraph_layout.bottom_inset_px > list_layout.bottom_inset_px);
|
||||
}
|
||||
|
||||
|
|
@ -1826,33 +1849,35 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_preview_code_rows_reserve_bottom_space_for_local_scrollbar() {
|
||||
let row = markdown_row(MarkdownPreviewRowKind::CodeLine {
|
||||
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 {
|
||||
is_first: false,
|
||||
is_last: true,
|
||||
});
|
||||
|
||||
let layout = markdown_preview_row_layout(&row);
|
||||
let first_layout = markdown_preview_row_layout(&first_row);
|
||||
let last_layout = markdown_preview_row_layout(&last_row);
|
||||
|
||||
assert_eq!(
|
||||
layout.shell_bottom_inset_px,
|
||||
super::MARKDOWN_PREVIEW_CODE_SCROLLBAR_PAD_BOTTOM_PX
|
||||
);
|
||||
assert_eq!(layout.bottom_inset_px, 4.0);
|
||||
assert_eq!(first_layout.top_inset_px, 5.0);
|
||||
assert_eq!(last_layout.bottom_inset_px, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_preview_top_level_code_rows_drop_outer_horizontal_padding() {
|
||||
fn markdown_preview_nested_code_rows_keep_small_outer_edge_gap() {
|
||||
let mut row = markdown_row(MarkdownPreviewRowKind::CodeLine {
|
||||
is_first: true,
|
||||
is_last: false,
|
||||
});
|
||||
row.indent_level = 0;
|
||||
row.indent_level = 3;
|
||||
|
||||
let padding = markdown_preview_row_horizontal_padding(&row);
|
||||
|
||||
assert_eq!(padding.left_px, 0.0);
|
||||
assert_eq!(padding.right_px, 0.0);
|
||||
assert_eq!(padding.left_px, super::MARKDOWN_PREVIEW_BOXED_EDGE_GAP_PX);
|
||||
assert_eq!(padding.right_px, super::MARKDOWN_PREVIEW_BOXED_EDGE_GAP_PX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2125,7 +2150,6 @@ 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gitcomet-ui"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gitcomet-app"
|
||||
version = "0.1.0"
|
||||
name = "gitcomet"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
|
@ -16,7 +16,7 @@ fn main() {
|
|||
let Some(install_dir) = current_exe.parent() else {
|
||||
return;
|
||||
};
|
||||
let app_exe = install_dir.join("gitcomet-app.exe");
|
||||
let app_exe = install_dir.join("gitcomet.exe");
|
||||
|
||||
let mut cmd = Command::new(app_exe);
|
||||
cmd.current_dir(install_dir);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! CLI argument parsing for gitcomet-app.
|
||||
//! CLI argument parsing for gitcomet.
|
||||
//!
|
||||
//! 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-app", about = "Git GUI built with GPUI", version)]
|
||||
#[command(name = "gitcomet", about = "Git GUI built with GPUI", version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
|
@ -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-app --${mode}"),
|
||||
!script.contains("cargo build -p gitcomet --${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-app".into(),
|
||||
"gitcomet".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-app".into(),
|
||||
"gitcomet".into(),
|
||||
"mergetool".into(),
|
||||
"--base=".into(),
|
||||
"--local".into(),
|
||||
|
|
@ -1437,14 +1437,7 @@ fn mergetool_env_only_resolution_without_base() {
|
|||
#[test]
|
||||
fn clap_parses_difftool_subcommand() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"gitcomet-app",
|
||||
"difftool",
|
||||
"--local",
|
||||
"/tmp/a",
|
||||
"--remote",
|
||||
"/tmp/b",
|
||||
"--path",
|
||||
"foo.txt",
|
||||
"gitcomet", "difftool", "--local", "/tmp/a", "--remote", "/tmp/b", "--path", "foo.txt",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -1461,7 +1454,7 @@ fn clap_parses_difftool_subcommand() {
|
|||
#[test]
|
||||
fn clap_parses_mergetool_subcommand() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"gitcomet-app",
|
||||
"gitcomet",
|
||||
"mergetool",
|
||||
"--merged",
|
||||
"/tmp/m",
|
||||
|
|
@ -1498,7 +1491,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-app",
|
||||
"gitcomet",
|
||||
"mergetool",
|
||||
merged_flag,
|
||||
"/tmp/m",
|
||||
|
|
@ -1523,7 +1516,7 @@ fn clap_parses_mergetool_output_aliases() {
|
|||
#[test]
|
||||
fn clap_parses_mergetool_kdiff3_label_aliases() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"gitcomet-app",
|
||||
"gitcomet",
|
||||
"mergetool",
|
||||
"--merged",
|
||||
"/tmp/m",
|
||||
|
|
@ -1552,7 +1545,7 @@ fn clap_parses_mergetool_kdiff3_label_aliases() {
|
|||
|
||||
#[test]
|
||||
fn clap_parses_setup_subcommand() {
|
||||
let cli = Cli::try_parse_from(["gitcomet-app", "setup", "--dry-run", "--local"]).unwrap();
|
||||
let cli = Cli::try_parse_from(["gitcomet", "setup", "--dry-run", "--local"]).unwrap();
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Setup(args)) => {
|
||||
|
|
@ -1565,7 +1558,7 @@ fn clap_parses_setup_subcommand() {
|
|||
|
||||
#[test]
|
||||
fn clap_parses_uninstall_subcommand() {
|
||||
let cli = Cli::try_parse_from(["gitcomet-app", "uninstall", "--dry-run", "--local"]).unwrap();
|
||||
let cli = Cli::try_parse_from(["gitcomet", "uninstall", "--dry-run", "--local"]).unwrap();
|
||||
|
||||
match cli.command {
|
||||
Some(Command::Uninstall(args)) => {
|
||||
|
|
@ -1581,7 +1574,7 @@ fn uninstall_mode_resolves_into_app_mode() {
|
|||
let env = TestEnv::new();
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("uninstall"),
|
||||
OsString::from("--dry-run"),
|
||||
OsString::from("--local"),
|
||||
|
|
@ -1601,14 +1594,14 @@ fn uninstall_mode_resolves_into_app_mode() {
|
|||
|
||||
#[test]
|
||||
fn clap_parses_no_subcommand_as_browser() {
|
||||
let cli = Cli::try_parse_from(["gitcomet-app"]).unwrap();
|
||||
let cli = Cli::try_parse_from(["gitcomet"]).unwrap();
|
||||
assert!(cli.command.is_none());
|
||||
assert!(cli.path.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clap_parses_path_argument() {
|
||||
let cli = Cli::try_parse_from(["gitcomet-app", "/some/repo"]).unwrap();
|
||||
let cli = Cli::try_parse_from(["gitcomet", "/some/repo"]).unwrap();
|
||||
assert!(cli.command.is_none());
|
||||
assert_eq!(
|
||||
cli.path.as_deref(),
|
||||
|
|
@ -1625,7 +1618,7 @@ fn compat_parses_positional_difftool_invocation() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
local.into_os_string(),
|
||||
remote.into_os_string(),
|
||||
],
|
||||
|
|
@ -1653,7 +1646,7 @@ fn compat_parses_kdiff3_style_difftool_labels() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--L1"),
|
||||
OsString::from("LEFT_LABEL"),
|
||||
OsString::from("--L2"),
|
||||
|
|
@ -1683,7 +1676,7 @@ fn compat_parses_kdiff3_style_difftool_short_numbered_equals_labels() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("-L1=LEFT_LABEL"),
|
||||
OsString::from("-L2=RIGHT_LABEL"),
|
||||
local.into_os_string(),
|
||||
|
|
@ -1711,7 +1704,7 @@ fn compat_parses_meld_style_difftool_short_labels() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("-L"),
|
||||
OsString::from("LEFT_LABEL"),
|
||||
OsString::from("--label"),
|
||||
|
|
@ -1741,7 +1734,7 @@ fn compat_parses_meld_style_difftool_attached_labels() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("-LLEFT_LABEL"),
|
||||
OsString::from("--label=RIGHT_LABEL"),
|
||||
local.into_os_string(),
|
||||
|
|
@ -1771,7 +1764,7 @@ fn compat_parses_kdiff3_style_mergetool_with_base() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("--L1"),
|
||||
OsString::from("BASE_LABEL"),
|
||||
|
|
@ -1814,7 +1807,7 @@ fn compat_parses_kdiff3_style_mergetool_with_base_flag() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("--L1=BASE_LABEL"),
|
||||
OsString::from("--L2=LOCAL_LABEL"),
|
||||
|
|
@ -1855,7 +1848,7 @@ fn compat_parses_kdiff3_style_mergetool_with_short_numbered_label_flags() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("-L1"),
|
||||
OsString::from("BASE_LABEL"),
|
||||
|
|
@ -1898,7 +1891,7 @@ fn compat_parses_kdiff3_style_mergetool_with_attached_short_numbered_label_flags
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("-L1BASE_LABEL"),
|
||||
OsString::from("-L2=LOCAL_LABEL"),
|
||||
|
|
@ -1938,7 +1931,7 @@ fn compat_parses_kdiff3_style_mergetool_with_attached_output_and_base_flags() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("--L1=BASE_LABEL"),
|
||||
OsString::from("--L2=LOCAL_LABEL"),
|
||||
|
|
@ -1976,7 +1969,7 @@ fn compat_parses_kdiff3_style_mergetool_without_base() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("--L1"),
|
||||
OsString::from("LOCAL_LABEL"),
|
||||
|
|
@ -2016,7 +2009,7 @@ fn compat_mergetool_applies_merge_conflictstyle_from_git_config() {
|
|||
|
||||
let mode = parse_mode_for_test_with_config(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("-o"),
|
||||
merged.into_os_string(),
|
||||
|
|
@ -2052,7 +2045,7 @@ fn compat_mergetool_applies_diff_algorithm_from_git_config() {
|
|||
|
||||
let mode = parse_mode_for_test_with_config(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
OsString::from("-o"),
|
||||
merged.into_os_string(),
|
||||
|
|
@ -2088,7 +2081,7 @@ fn compat_parses_meld_style_mergetool_with_output() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--output"),
|
||||
merged.clone().into_os_string(),
|
||||
local.clone().into_os_string(),
|
||||
|
|
@ -2124,7 +2117,7 @@ fn compat_parses_meld_style_mergetool_labels() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--output"),
|
||||
merged.clone().into_os_string(),
|
||||
OsString::from("--label=LOCAL_LABEL"),
|
||||
|
|
@ -2164,7 +2157,7 @@ fn compat_parses_meld_style_mergetool_with_auto_merge_flag() {
|
|||
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto-merge"),
|
||||
OsString::from("--output"),
|
||||
merged.clone().into_os_string(),
|
||||
|
|
@ -2197,7 +2190,7 @@ fn compat_auto_merge_requires_output_path() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto-merge"),
|
||||
local.into_os_string(),
|
||||
base.into_os_string(),
|
||||
|
|
@ -2224,7 +2217,7 @@ fn compat_rejects_too_many_label_flags() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--output"),
|
||||
merged.into_os_string(),
|
||||
OsString::from("--label"),
|
||||
|
|
@ -2258,7 +2251,7 @@ fn compat_auto_requires_output_path() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--auto"),
|
||||
local.into_os_string(),
|
||||
remote.into_os_string(),
|
||||
|
|
@ -2282,7 +2275,7 @@ fn compat_merge_requires_two_or_three_positionals_after_output_flag() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--output"),
|
||||
merged.into_os_string(),
|
||||
local.into_os_string(),
|
||||
|
|
@ -2309,7 +2302,7 @@ fn compat_merge_rejects_too_many_positionals() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--out"),
|
||||
merged.into_os_string(),
|
||||
base.into_os_string(),
|
||||
|
|
@ -2338,7 +2331,7 @@ fn compat_merge_rejects_base_flag_with_extra_positionals() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--base"),
|
||||
base.into_os_string(),
|
||||
OsString::from("--out"),
|
||||
|
|
@ -2368,7 +2361,7 @@ fn compat_merge_without_base_rejects_l3_label() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--out"),
|
||||
merged.into_os_string(),
|
||||
OsString::from("--L3"),
|
||||
|
|
@ -2395,7 +2388,7 @@ fn compat_diff_rejects_l3_without_output_path() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--L3"),
|
||||
OsString::from("REMOTE"),
|
||||
local.into_os_string(),
|
||||
|
|
@ -2421,7 +2414,7 @@ fn compat_diff_rejects_base_without_output_path() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--base"),
|
||||
base.into_os_string(),
|
||||
local.into_os_string(),
|
||||
|
|
@ -2447,7 +2440,7 @@ fn compat_diff_rejects_too_many_positionals() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
local.into_os_string(),
|
||||
remote.into_os_string(),
|
||||
extra.into_os_string(),
|
||||
|
|
@ -2594,7 +2587,7 @@ fn compat_base_flag_requires_two_positionals_when_output_present() {
|
|||
|
||||
let err = parse_mode_for_test(
|
||||
vec![
|
||||
OsString::from("gitcomet-app"),
|
||||
OsString::from("gitcomet"),
|
||||
OsString::from("--base"),
|
||||
base.into_os_string(),
|
||||
OsString::from("--out"),
|
||||
|
|
@ -2834,7 +2827,7 @@ fn mergetool_marker_size_zero_errors() {
|
|||
#[test]
|
||||
fn clap_parses_conflict_style_and_diff_algorithm() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"gitcomet-app",
|
||||
"gitcomet",
|
||||
"mergetool",
|
||||
"--merged",
|
||||
"/tmp/m",
|
||||
|
|
@ -2996,7 +2989,7 @@ fn git_config_fallback_combined_style_and_algorithm() {
|
|||
#[test]
|
||||
fn clap_parses_mergetool_auto_flag() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"gitcomet-app",
|
||||
"gitcomet",
|
||||
"mergetool",
|
||||
"--merged",
|
||||
"/tmp/m",
|
||||
|
|
@ -3019,7 +3012,7 @@ fn clap_parses_mergetool_auto_flag() {
|
|||
#[test]
|
||||
fn clap_parses_mergetool_auto_merge_alias_flag() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"gitcomet-app",
|
||||
"gitcomet",
|
||||
"mergetool",
|
||||
"--merged",
|
||||
"/tmp/m",
|
||||
|
|
@ -3133,7 +3126,7 @@ fn compat_auto_merge_flag_propagates_to_config() {
|
|||
#[test]
|
||||
fn clap_parses_extract_merge_fixtures_subcommand() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"gitcomet-app",
|
||||
"gitcomet",
|
||||
"extract-merge-fixtures",
|
||||
"--repo",
|
||||
"/tmp/repo",
|
||||
|
|
@ -3162,7 +3155,7 @@ fn extract_merge_fixtures_mode_resolves_into_app_mode() {
|
|||
let env = TestEnv::new();
|
||||
let mode = parse_mode_for_test(
|
||||
vec![
|
||||
"gitcomet-app".into(),
|
||||
"gitcomet".into(),
|
||||
"extract-merge-fixtures".into(),
|
||||
"--repo".into(),
|
||||
"/tmp/repo".into(),
|
||||
|
|
@ -568,7 +568,7 @@ mod tests {
|
|||
fn parse_crash_log_extracts_fields() {
|
||||
let log = r#"=== GitComet crash (panic) ===
|
||||
timestamp_unix_ms=123
|
||||
crate=gitcomet-app version=0.1.0
|
||||
crate=gitcomet 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-app"));
|
||||
assert_eq!(parsed.crate_name.as_deref(), Some("gitcomet"));
|
||||
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-app version=0.1.0
|
||||
crate=gitcomet 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-app version=0.1.0
|
||||
crate=gitcomet version=0.1.0
|
||||
thread=main
|
||||
location=src/main.rs#L42
|
||||
message=boom happened
|
||||
|
|
@ -148,7 +148,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-app --features ui-gpui`."
|
||||
"GUI difftool mode is unavailable in this build. Rebuild with `-p gitcomet --features ui-gpui`."
|
||||
);
|
||||
std::process::exit(exit_code::ERROR);
|
||||
}
|
||||
|
|
@ -234,7 +234,7 @@ fn main() {
|
|||
#[cfg(not(feature = "ui"))]
|
||||
{
|
||||
let _ = path;
|
||||
eprintln!("GitComet UI is disabled. Build with `-p gitcomet-app --features ui`.");
|
||||
eprintln!("GitComet UI is disabled. Build with `-p gitcomet --features ui`.");
|
||||
std::process::exit(exit_code::ERROR);
|
||||
}
|
||||
}
|
||||
|
|
@ -242,7 +242,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-app --features ui-gpui`."
|
||||
"GUI mergetool mode is unavailable in this build. Rebuild with `-p gitcomet --features ui-gpui`."
|
||||
);
|
||||
std::process::exit(exit_code::ERROR);
|
||||
}
|
||||
|
|
@ -284,6 +284,45 @@ 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() {
|
||||
|
|
@ -293,23 +332,33 @@ fn maybe_relaunch_browser_from_macos_app_bundle() -> bool {
|
|||
let Ok(current_exe) = std::env::current_exe() else {
|
||||
return false;
|
||||
};
|
||||
if current_exe
|
||||
.to_string_lossy()
|
||||
.contains(".app/Contents/MacOS/")
|
||||
{
|
||||
let resolved_exe = resolve_executable_path_for_bundle_detection(¤t_exe);
|
||||
if is_macos_app_bundle_executable(&resolved_exe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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(¤t_exe, &app_bundle) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to prepare macOS app bundle icon: {err}");
|
||||
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(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);
|
||||
|
|
@ -336,7 +385,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-app");
|
||||
let app_exe = macos.join("gitcomet");
|
||||
std::fs::copy(current_exe, &app_exe)
|
||||
.map_err(|e| format!("failed to copy executable into bundle: {e}"))?;
|
||||
|
||||
|
|
@ -372,7 +421,7 @@ fn ensure_macos_dev_app_bundle(
|
|||
<key>CFBundleDisplayName</key>
|
||||
<string>GitComet</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>gitcomet-app</string>
|
||||
<string>gitcomet</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ai.autoexplore.gitcomet.dev</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
|
|
@ -680,6 +729,66 @@ 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 {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! `gitcomet-app setup` / `gitcomet-app uninstall` support.
|
||||
//! `gitcomet setup` / `gitcomet 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-app binary path: {e}"))
|
||||
.map_err(|e| format!("Cannot determine gitcomet 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-app");
|
||||
let entries = build_config_entries("/usr/bin/gitcomet");
|
||||
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-app");
|
||||
let entries = build_config_entries("/usr/bin/gitcomet");
|
||||
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-app");
|
||||
let entries = build_config_entries("/tmp/it's/gitcomet");
|
||||
let cmd = entries
|
||||
.iter()
|
||||
.find(|e| e.key == "mergetool.gitcomet.cmd")
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
cmd.value.starts_with("'/tmp/it'\"'\"'s/gitcomet-app'"),
|
||||
cmd.value.starts_with("'/tmp/it'\"'\"'s/gitcomet'"),
|
||||
"unexpected cmd quoting: {}",
|
||||
cmd.value
|
||||
);
|
||||
|
|
@ -1076,7 +1076,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn format_commands_global_scope() {
|
||||
let entries = build_config_entries("/bin/gitcomet-app");
|
||||
let entries = build_config_entries("/bin/gitcomet");
|
||||
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-app'"),
|
||||
!output.contains("''/bin/gitcomet'"),
|
||||
"dry-run output should not contain broken nested quoting:\n{output}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_commands_local_scope() {
|
||||
let entries = build_config_entries("/bin/gitcomet-app");
|
||||
let entries = build_config_entries("/bin/gitcomet");
|
||||
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-app");
|
||||
let entries = build_config_entries("/test/gitcomet");
|
||||
let result = std::process::Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(dir.path())
|
||||
|
|
@ -59,7 +59,7 @@ fn require_git_shell_for_tool_tests() -> bool {
|
|||
}
|
||||
|
||||
fn gitcomet_bin() -> PathBuf {
|
||||
for env_key in ["CARGO_BIN_EXE_gitcomet-app", "CARGO_BIN_EXE_gitcomet_app"] {
|
||||
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
|
||||
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
|
||||
if path.is_file() {
|
||||
return path;
|
||||
|
|
@ -72,8 +72,7 @@ fn gitcomet_bin() -> PathBuf {
|
|||
}
|
||||
|
||||
panic!(
|
||||
"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"
|
||||
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +82,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-app", "gitcomet_app"] {
|
||||
for bin_name in ["gitcomet"] {
|
||||
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
|
|
@ -264,7 +263,7 @@ fn output_text(output: &Output) -> String {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn git_difftool_invokes_gitcomet_app_for_basic_diff() {
|
||||
fn git_difftool_invokes_gitcomet_for_basic_diff() {
|
||||
if !require_git_shell_for_tool_tests() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -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-app", "CARGO_BIN_EXE_gitcomet_app"] {
|
||||
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
|
||||
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
|
||||
if path.is_file() {
|
||||
return path;
|
||||
|
|
@ -30,8 +30,7 @@ fn gitcomet_bin() -> PathBuf {
|
|||
}
|
||||
|
||||
panic!(
|
||||
"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"
|
||||
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +40,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-app", "gitcomet_app"] {
|
||||
for bin_name in ["gitcomet"] {
|
||||
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
|
|
@ -59,7 +58,7 @@ where
|
|||
Command::new(gitcomet_bin())
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("gitcomet-app command to run")
|
||||
.expect("gitcomet command to run")
|
||||
}
|
||||
|
||||
fn run_git_capture(repo: &Path, args: &[&str]) -> Output {
|
||||
|
|
@ -62,7 +62,7 @@ fn require_git_shell_for_tool_tests() -> bool {
|
|||
}
|
||||
|
||||
fn gitcomet_bin() -> PathBuf {
|
||||
for env_key in ["CARGO_BIN_EXE_gitcomet-app", "CARGO_BIN_EXE_gitcomet_app"] {
|
||||
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
|
||||
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
|
||||
if path.is_file() {
|
||||
return path;
|
||||
|
|
@ -75,8 +75,7 @@ fn gitcomet_bin() -> PathBuf {
|
|||
}
|
||||
|
||||
panic!(
|
||||
"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"
|
||||
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +85,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-app", "gitcomet_app"] {
|
||||
for bin_name in ["gitcomet"] {
|
||||
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
|
|
@ -650,11 +649,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-app mergetool\noutput:\n{text}"
|
||||
"expected alias flags to be accepted by gitcomet mergetool\noutput:\n{text}"
|
||||
);
|
||||
assert!(
|
||||
text.contains("Auto-merging file.txt"),
|
||||
"expected gitcomet-app mergetool to run\noutput:\n{text}"
|
||||
"expected gitcomet mergetool to run\noutput:\n{text}"
|
||||
);
|
||||
assert!(
|
||||
merged.contains("LOCAL") || merged.contains("REMOTE") || merged.contains("<<<<<<<"),
|
||||
|
|
@ -1180,7 +1179,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-app and git mergetool.
|
||||
// contract between gitcomet and git mergetool.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo = tmp.path();
|
||||
|
||||
|
|
@ -3677,7 +3676,7 @@ fn gitcomet_mergetool_reads_conflictstyle_from_repo_when_cwd_is_outside_repo() {
|
|||
.arg("--merged")
|
||||
.arg(&merged)
|
||||
.output()
|
||||
.expect("gitcomet-app mergetool command to run");
|
||||
.expect("gitcomet mergetool command to run");
|
||||
|
||||
let text = output_text(&output);
|
||||
let merged_text = fs::read_to_string(&merged).unwrap_or_else(|e| {
|
||||
|
|
@ -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-app", "CARGO_BIN_EXE_gitcomet_app"] {
|
||||
for env_key in ["CARGO_BIN_EXE_gitcomet"] {
|
||||
if let Some(path) = std::env::var_os(env_key).map(PathBuf::from) {
|
||||
if path.is_file() {
|
||||
return path;
|
||||
|
|
@ -19,8 +19,7 @@ fn gitcomet_bin() -> PathBuf {
|
|||
}
|
||||
|
||||
panic!(
|
||||
"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"
|
||||
"gitcomet binary path was not found. Tried CARGO_BIN_EXE_gitcomet and a fallback relative to current test executable"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +29,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-app", "gitcomet_app"] {
|
||||
for bin_name in ["gitcomet"] {
|
||||
let candidate = profile_dir.join(format!("{bin_name}{exe_suffix}"));
|
||||
if candidate.is_file() {
|
||||
return Some(candidate);
|
||||
|
|
@ -48,7 +47,7 @@ where
|
|||
Command::new(gitcomet_bin())
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("gitcomet-app command to run")
|
||||
.expect("gitcomet command to run")
|
||||
}
|
||||
|
||||
fn run_gitcomet_in_dir<I, S>(dir: &Path, args: I) -> Output
|
||||
|
|
@ -60,7 +59,7 @@ where
|
|||
.current_dir(dir)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("gitcomet-app command to run")
|
||||
.expect("gitcomet command to run")
|
||||
}
|
||||
|
||||
fn run_gitcomet_in_dir_with_global_env<I, S>(
|
||||
|
|
@ -78,7 +77,7 @@ where
|
|||
.current_dir(dir)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("gitcomet-app command to run")
|
||||
.expect("gitcomet command to run")
|
||||
}
|
||||
|
||||
fn git_config_get(repo_dir: &Path, key: &str) -> Option<String> {
|
||||
|
|
@ -2447,7 +2446,7 @@ fn standalone_mergetool_auto_crlf_subchunk_preserves_line_endings() {
|
|||
|
||||
// ── Setup E2E integration ────────────────────────────────────────────
|
||||
//
|
||||
// These tests verify that `gitcomet-app setup --local` produces config that
|
||||
// These tests verify that `gitcomet 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
|
||||
|
|
@ -2592,12 +2591,12 @@ fn git_config_get_global_with_env(env: &IsolatedGlobalGitEnv, key: &str) -> Opti
|
|||
}
|
||||
}
|
||||
|
||||
/// After `gitcomet-app setup --local`, `git mergetool` should invoke
|
||||
/// gitcomet-app's built-in 3-way merge for conflicted files.
|
||||
/// After `gitcomet setup --local`, `git mergetool` should invoke
|
||||
/// gitcomet's built-in 3-way merge for conflicted files.
|
||||
///
|
||||
/// For a true content conflict, gitcomet-app exits 1 and git mergetool
|
||||
/// For a true content conflict, gitcomet exits 1 and git mergetool
|
||||
/// restores the original file (expected behavior with trustExitCode=true).
|
||||
/// We verify the tool was invoked by checking for gitcomet-app's specific
|
||||
/// We verify the tool was invoked by checking for gitcomet's specific
|
||||
/// stderr messages, which differ from git's own merge output.
|
||||
#[test]
|
||||
fn setup_local_enables_git_mergetool_end_to_end() {
|
||||
|
|
@ -2631,33 +2630,33 @@ fn setup_local_enables_git_mergetool_end_to_end() {
|
|||
"expected merge conflict but git merge succeeded"
|
||||
);
|
||||
|
||||
// 3. Run `git mergetool` — should invoke gitcomet-app via setup config.
|
||||
// 3. Run `git mergetool` — should invoke gitcomet 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-app was invoked by checking for its specific stderr
|
||||
// messages. "conflict(s) remain" is emitted by gitcomet-app's mergetool
|
||||
// 4. Verify gitcomet was invoked by checking for its specific stderr
|
||||
// messages. "conflict(s) remain" is emitted by gitcomet'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-app's conflict message in mergetool stderr\n\
|
||||
"expected gitcomet's conflict message in mergetool stderr\n\
|
||||
stdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&mt.stdout),
|
||||
mt_stderr,
|
||||
);
|
||||
|
||||
// Also verify gitcomet-app's "CONFLICT (content)" message is present.
|
||||
// Also verify gitcomet's "CONFLICT (content)" message is present.
|
||||
assert!(
|
||||
mt_stderr.contains("CONFLICT (content)"),
|
||||
"expected CONFLICT marker from gitcomet-app\nstderr: {}",
|
||||
"expected CONFLICT marker from gitcomet\nstderr: {}",
|
||||
mt_stderr,
|
||||
);
|
||||
}
|
||||
|
||||
/// After `gitcomet-app setup --local`, `git difftool` should invoke
|
||||
/// gitcomet-app's built-in diff and produce unified diff output.
|
||||
/// After `gitcomet setup --local`, `git difftool` should invoke
|
||||
/// gitcomet'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() {
|
||||
|
|
@ -2677,7 +2676,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-app difftool.
|
||||
// 3. Run `git difftool` — should invoke gitcomet 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);
|
||||
|
|
@ -2688,7 +2687,7 @@ fn setup_local_enables_git_difftool_end_to_end() {
|
|||
"git difftool should exit 0\n{dt_text}"
|
||||
);
|
||||
|
||||
// gitcomet-app difftool produces unified diff output.
|
||||
// gitcomet difftool produces unified diff output.
|
||||
let stdout = String::from_utf8_lossy(&dt.stdout);
|
||||
assert!(
|
||||
stdout.contains("@@"),
|
||||
|
|
@ -2704,7 +2703,7 @@ fn setup_local_enables_git_difftool_end_to_end() {
|
|||
);
|
||||
}
|
||||
|
||||
/// After `gitcomet-app setup --local`, quoted stage variables in the generated
|
||||
/// After `gitcomet 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() {
|
||||
|
|
@ -2760,7 +2759,7 @@ fn setup_local_mergetool_handles_spaced_unicode_path_end_to_end() {
|
|||
);
|
||||
}
|
||||
|
||||
/// After `gitcomet-app setup --local`, quoted stage variables in the generated
|
||||
/// After `gitcomet 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() {
|
||||
|
|
@ -2808,7 +2807,7 @@ fn setup_local_difftool_handles_spaced_unicode_path_end_to_end() {
|
|||
);
|
||||
}
|
||||
|
||||
/// `gitcomet-app setup` (global scope) should configure an isolated global
|
||||
/// `gitcomet 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() {
|
||||
|
|
@ -2885,7 +2884,7 @@ fn setup_global_enables_git_mergetool_end_to_end_with_isolated_global_config() {
|
|||
);
|
||||
}
|
||||
|
||||
/// `gitcomet-app setup` (global scope) should configure an isolated global
|
||||
/// `gitcomet 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() {
|
||||
|
|
@ -2942,7 +2941,7 @@ fn setup_global_enables_git_difftool_end_to_end_with_isolated_global_config() {
|
|||
);
|
||||
}
|
||||
|
||||
/// `gitcomet-app setup` (global scope) should make both headless and GUI
|
||||
/// `gitcomet 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() {
|
||||
|
|
@ -2994,7 +2993,7 @@ fn setup_global_mergetool_tool_help_lists_headless_and_gui_entries() {
|
|||
);
|
||||
}
|
||||
|
||||
/// `gitcomet-app setup` (global scope) should make both headless and GUI
|
||||
/// `gitcomet 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() {
|
||||
|
|
@ -3058,7 +3057,7 @@ fn help_flag_exits_zero() {
|
|||
);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(
|
||||
stdout.contains("gitcomet-app"),
|
||||
stdout.contains("gitcomet"),
|
||||
"help output should mention the binary name"
|
||||
);
|
||||
}
|
||||
|
|
@ -3073,7 +3072,7 @@ fn version_flag_exits_zero() {
|
|||
);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(
|
||||
stdout.contains("gitcomet-app"),
|
||||
stdout.contains("gitcomet"),
|
||||
"version output should mention the binary name"
|
||||
);
|
||||
}
|
||||
|
|
@ -60,16 +60,16 @@
|
|||
<Component Id='MainBinary' Guid='*'>
|
||||
<File
|
||||
Id='MainExeFile'
|
||||
Name='gitcomet-app.exe'
|
||||
Name='gitcomet.exe'
|
||||
DiskId='1'
|
||||
Source='$(var.CargoTargetBinDir)\gitcomet-app.exe'
|
||||
Source='$(var.CargoTargetBinDir)\gitcomet.exe'
|
||||
KeyPath='yes'/>
|
||||
</Component>
|
||||
|
||||
<Component Id='LauncherBinary' Guid='*'>
|
||||
<File
|
||||
Id='LauncherExeFile'
|
||||
Name='GitComet.exe'
|
||||
Name='gitcomet-launcher.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.exe'
|
||||
Target='[Bin]gitcomet-launcher.exe'
|
||||
WorkingDirectory='APPLICATIONFOLDER'
|
||||
Icon='ProductICO'
|
||||
IconIndex='0'
|
||||
|
|
@ -199,9 +199,10 @@ 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}"
|
||||
|
|
@ -211,7 +212,6 @@ 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,12 +220,11 @@ 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
RUSTFLAGS="-C symbol-mangling-version=v0" cargo build -p gitcomet-app --all-targets --profile=release-with-debug
|
||||
RUSTFLAGS="-C symbol-mangling-version=v0" cargo build -p gitcomet --all-targets --profile=release-with-debug
|
||||
|
|
|
|||
122
scripts/generate-homebrew-cask.sh
Executable file
122
scripts/generate-homebrew-cask.sh
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
#!/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"
|
||||
|
|
@ -110,8 +110,8 @@ linux_name="$(basename "$linux_tar")"
|
|||
mkdir -p "$(dirname "$out_path")"
|
||||
|
||||
cat > "$out_path" <<EOF2
|
||||
class Gitcomet < Formula
|
||||
desc "Fast, resource-efficient Git GUI written in Rust"
|
||||
class GitcometCli < Formula
|
||||
desc "GitComet command-line binary"
|
||||
homepage "https://github.com/${github_repo}"
|
||||
version "${version}"
|
||||
license "AGPL-3.0-only"
|
||||
|
|
@ -136,11 +136,11 @@ class Gitcomet < Formula
|
|||
end
|
||||
|
||||
def install
|
||||
bin.install Dir["**/gitcomet-app"].fetch(0)
|
||||
bin.install "gitcomet"
|
||||
end
|
||||
|
||||
test do
|
||||
assert_match "Usage", shell_output("#{bin}/gitcomet-app --help")
|
||||
assert_match "Usage", shell_output("#{bin}/gitcomet --help")
|
||||
end
|
||||
end
|
||||
EOF2
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ usage() {
|
|||
Usage: scripts/install-linux.sh [--release|--debug] [--prefix PATH] [--no-build]
|
||||
|
||||
Installs:
|
||||
- binary to <prefix>/bin/gitcomet-app
|
||||
- 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
|
||||
sizes: 32, 48, 128, 256, 512
|
||||
|
|
@ -32,14 +32,14 @@ while [[ $# -gt 0 ]]; do
|
|||
done
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
bin_src="${repo_root}/target/${mode}/gitcomet-app"
|
||||
bin_src="${repo_root}/target/${mode}/gitcomet"
|
||||
|
||||
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-app "${cargo_mode_flag[@]}")
|
||||
(cd "$repo_root" && cargo build -p gitcomet "${cargo_mode_flag[@]}")
|
||||
fi
|
||||
|
||||
if [[ ! -x "$bin_src" ]]; then
|
||||
|
|
@ -53,12 +53,12 @@ appdir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
|
|||
iconsroot="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor"
|
||||
icon_sizes=(32 48 128 256 512)
|
||||
|
||||
install -Dm755 "$bin_src" "${bindir}/gitcomet-app"
|
||||
install -Dm755 "$bin_src" "${bindir}/gitcomet"
|
||||
|
||||
# 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-app|g" \
|
||||
sed "s|^Exec=.*$|Exec=${bindir}/gitcomet|g" \
|
||||
"${repo_root}/assets/linux/gitcomet.desktop" >"$tmp_desktop"
|
||||
install -Dm644 "$tmp_desktop" "${appdir}/gitcomet.desktop"
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$
|
|||
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-app"
|
||||
echo " ${bindir}/gitcomet"
|
||||
echo " ${appdir}/gitcomet.desktop"
|
||||
for size in "${icon_sizes[@]}"; do
|
||||
echo " ${iconsroot}/${size}x${size}/apps/gitcomet.png"
|
||||
|
|
|
|||
|
|
@ -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-app"
|
||||
bin_src="${repo_root}/target/${mode}/gitcomet"
|
||||
|
||||
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-app "${cargo_mode_flag[@]}" --locked --features ui-gpui,gix --bins)
|
||||
(cd "$repo_root" && cargo build -p gitcomet "${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-app"
|
||||
install -m755 "$bin_src" "${release_dir}/gitcomet-app"
|
||||
install -m755 "$bin_src" "${macos_dir}/gitcomet"
|
||||
install -m755 "$bin_src" "${release_dir}/gitcomet"
|
||||
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,7 +158,7 @@ cat > "${contents_dir}/Info.plist" <<PLIST
|
|||
<key>CFBundleDisplayName</key>
|
||||
<string>GitComet</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>gitcomet-app</string>
|
||||
<string>gitcomet</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ai.autoexplore.gitcomet</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
cargo wix crates\gitcomet-app\Cargo.toml -p gitcomet-app --profile release --nocapture --output dist\gitcomet-local-test.msi
|
||||
cargo wix crates\gitcomet\Cargo.toml -p gitcomet --profile release --nocapture --output dist\gitcomet-local-test.msi
|
||||
|
|
@ -18,7 +18,7 @@ appdir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
|
|||
iconsroot="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor"
|
||||
icon_sizes=(32 48 128 256 512)
|
||||
|
||||
rm -f "${bindir}/gitcomet-app"
|
||||
rm -f "${bindir}/gitcomet"
|
||||
rm -f "${appdir}/gitcomet.desktop"
|
||||
for size in "${icon_sizes[@]}"; do
|
||||
rm -f "${iconsroot}/${size}x${size}/apps/gitcomet.png"
|
||||
|
|
|
|||
151
scripts/update-aur.sh
Executable file
151
scripts/update-aur.sh
Executable file
|
|
@ -0,0 +1,151 @@
|
|||
#!/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"
|
||||
|
|
@ -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-app
|
||||
Default: ./target/release-with-debug/gitcomet
|
||||
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-app}"
|
||||
binary="${2:-./target/release-with-debug/gitcomet}"
|
||||
valgrind \
|
||||
--tool=callgrind \
|
||||
--callgrind-out-file=callgrind.out \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue