mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-22 09:01:45 +03:00
Merge branch 'master' into ndom91/add-nix-flake
This commit is contained in:
commit
214667b17b
8
.github/actions/check-crate/action.yaml
vendored
8
.github/actions/check-crate/action.yaml
vendored
@ -15,6 +15,10 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: ./.github/actions/init-env-rust
|
- uses: ./.github/actions/init-env-rust
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
cargo build --locked -p gitbutler-git --bins
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
printf '%s\n' "$JSON_DOC" > /tmp/features.json
|
printf '%s\n' "$JSON_DOC" > /tmp/features.json
|
||||||
cat /tmp/features.json | jq -r 'if . == "*" then "--all-features" elif . == "" then "" elif type == "array" then if length == 0 then "--no-default-features" else "--no-default-features --features " + join(",") end else . end' > /tmp/features
|
cat /tmp/features.json | jq -r 'if . == "*" then "--all-features" elif . == "" then "" elif type == "array" then if length == 0 then "--no-default-features" else "--no-default-features --features " + join(",") end else . end' > /tmp/features
|
||||||
@ -23,7 +27,9 @@ runs:
|
|||||||
FEATURES: ${{ inputs.features }}
|
FEATURES: ${{ inputs.features }}
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- run: cargo test --locked -p ${{ inputs.crate }} --all-targets $(cat /tmp/features)
|
- run: |
|
||||||
|
cargo build -p gitbutler-git --bins
|
||||||
|
cargo test --locked -p ${{ inputs.crate }} --all-targets $(cat /tmp/features)
|
||||||
if: inputs.action == 'test'
|
if: inputs.action == 'test'
|
||||||
env:
|
env:
|
||||||
GITBUTLER_TESTS_NO_CLEANUP: "1"
|
GITBUTLER_TESTS_NO_CLEANUP: "1"
|
||||||
|
7
.github/actions/init-env-rust/action.yaml
vendored
7
.github/actions/init-env-rust/action.yaml
vendored
@ -3,13 +3,6 @@ description: prepare runner for rust related tasks
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Cache rust dependencies
|
|
||||||
if: runner.name != 'ScottsMacStudio' # internet in berlin is very slow
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
prefix-key: gitbutler-client
|
|
||||||
shared-key: rust
|
|
||||||
|
|
||||||
- name: Check versions
|
- name: Check versions
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
10
.github/workflows/publish.yaml
vendored
10
.github/workflows/publish.yaml
vendored
@ -29,9 +29,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-private-repositories
|
||||||
platform:
|
platform:
|
||||||
- macos-latest # [macOs, x64]
|
- macos-13 # [macOs, x64]
|
||||||
- macos-latest-xlarge # [macOs, ARM64]
|
- macos-latest # [macOs, ARM64]
|
||||||
- ubuntu-20.04 # [linux, x64]
|
- ubuntu-20.04 # [linux, x64]
|
||||||
- windows-latest # [windows, x64]
|
- windows-latest # [windows, x64]
|
||||||
|
|
||||||
@ -191,9 +192,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-private-repositories
|
||||||
platform:
|
platform:
|
||||||
- macos-latest # [macOs, x64]
|
- macos-13 # [macOs, x64]
|
||||||
- macos-latest-xlarge # [macOs, ARM64]
|
- macos-latest # [macOs, ARM64]
|
||||||
- ubuntu-20.04 # [linux, x64]
|
- ubuntu-20.04 # [linux, x64]
|
||||||
- windows-latest # [windows, x64]
|
- windows-latest # [windows, x64]
|
||||||
steps:
|
steps:
|
||||||
|
50
.github/workflows/push.yaml
vendored
50
.github/workflows/push.yaml
vendored
@ -18,6 +18,7 @@ jobs:
|
|||||||
gitbutler-tauri: ${{ steps.filter.outputs.gitbutler-tauri }}
|
gitbutler-tauri: ${{ steps.filter.outputs.gitbutler-tauri }}
|
||||||
gitbutler-changeset: ${{ steps.filter.outputs.gitbutler-changeset }}
|
gitbutler-changeset: ${{ steps.filter.outputs.gitbutler-changeset }}
|
||||||
gitbutler-git: ${{ steps.filter.outputs.gitbutler-git }}
|
gitbutler-git: ${{ steps.filter.outputs.gitbutler-git }}
|
||||||
|
gitbutler-cli: ${{ steps.filter.outputs.gitbutler-cli }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@v3
|
||||||
@ -51,6 +52,9 @@ jobs:
|
|||||||
gitbutler-git:
|
gitbutler-git:
|
||||||
- *rust
|
- *rust
|
||||||
- 'crates/gitbutler-git/**'
|
- 'crates/gitbutler-git/**'
|
||||||
|
gitbutler-cli:
|
||||||
|
- *rust
|
||||||
|
- 'crates/gitbutler-cli/**'
|
||||||
|
|
||||||
lint-node:
|
lint-node:
|
||||||
needs: changes
|
needs: changes
|
||||||
@ -79,7 +83,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/init-env-node
|
- uses: ./.github/actions/init-env-node
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|
||||||
rust-init:
|
rust-lint:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -89,7 +93,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/init-env-rust
|
- uses: ./.github/actions/init-env-rust
|
||||||
- run: cargo fmt --check --all
|
- run: cargo fmt --check --all
|
||||||
- run: cargo build --locked --all-targets --tests
|
|
||||||
|
|
||||||
rust-docs:
|
rust-docs:
|
||||||
needs: changes
|
needs: changes
|
||||||
@ -107,7 +110,7 @@ jobs:
|
|||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
|
|
||||||
check-gitbutler-tauri:
|
check-gitbutler-tauri:
|
||||||
needs: [changes, rust-init]
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.gitbutler-tauri == 'true' }}
|
if: ${{ needs.changes.outputs.gitbutler-tauri == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
@ -123,6 +126,7 @@ jobs:
|
|||||||
- [devtools]
|
- [devtools]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/init-env-rust
|
||||||
- uses: ./.github/actions/check-crate
|
- uses: ./.github/actions/check-crate
|
||||||
with:
|
with:
|
||||||
crate: gitbutler-tauri
|
crate: gitbutler-tauri
|
||||||
@ -130,7 +134,7 @@ jobs:
|
|||||||
action: ${{ matrix.action }}
|
action: ${{ matrix.action }}
|
||||||
|
|
||||||
check-gitbutler-changeset:
|
check-gitbutler-changeset:
|
||||||
needs: [changes, rust-init]
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.gitbutler-changeset == 'true' }}
|
if: ${{ needs.changes.outputs.gitbutler-changeset == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
@ -147,6 +151,7 @@ jobs:
|
|||||||
- []
|
- []
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/init-env-rust
|
||||||
- uses: ./.github/actions/check-crate
|
- uses: ./.github/actions/check-crate
|
||||||
with:
|
with:
|
||||||
crate: gitbutler-changeset
|
crate: gitbutler-changeset
|
||||||
@ -154,7 +159,7 @@ jobs:
|
|||||||
action: ${{ matrix.action }}
|
action: ${{ matrix.action }}
|
||||||
|
|
||||||
check-gitbutler-git:
|
check-gitbutler-git:
|
||||||
needs: [changes, rust-init]
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.gitbutler-git == 'true' }}
|
if: ${{ needs.changes.outputs.gitbutler-git == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
@ -171,6 +176,7 @@ jobs:
|
|||||||
- [tokio]
|
- [tokio]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/init-env-rust
|
||||||
- uses: ./.github/actions/check-crate
|
- uses: ./.github/actions/check-crate
|
||||||
with:
|
with:
|
||||||
crate: gitbutler-git
|
crate: gitbutler-git
|
||||||
@ -178,7 +184,7 @@ jobs:
|
|||||||
action: ${{ matrix.action }}
|
action: ${{ matrix.action }}
|
||||||
|
|
||||||
check-gitbutler-core:
|
check-gitbutler-core:
|
||||||
needs: [changes, rust-init]
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.gitbutler-core == 'true' }}
|
if: ${{ needs.changes.outputs.gitbutler-core == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
@ -194,12 +200,38 @@ jobs:
|
|||||||
- []
|
- []
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/init-env-rust
|
||||||
- uses: ./.github/actions/check-crate
|
- uses: ./.github/actions/check-crate
|
||||||
with:
|
with:
|
||||||
crate: gitbutler-core
|
crate: gitbutler-core
|
||||||
features: ${{ toJson(matrix.features) }}
|
features: ${{ toJson(matrix.features) }}
|
||||||
action: ${{ matrix.action }}
|
action: ${{ matrix.action }}
|
||||||
|
|
||||||
|
check-gitbutler-cli:
|
||||||
|
needs: changes
|
||||||
|
if: ${{ needs.changes.outputs.gitbutler-cli == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: ghcr.io/gitbutlerapp/ci-base-image:latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
action:
|
||||||
|
- test
|
||||||
|
- check
|
||||||
|
- check-tests
|
||||||
|
features:
|
||||||
|
- ''
|
||||||
|
- '*'
|
||||||
|
- []
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/init-env-rust
|
||||||
|
- uses: ./.github/actions/check-crate
|
||||||
|
with:
|
||||||
|
crate: gitbutler-cli
|
||||||
|
features: ${{ toJson(matrix.features) }}
|
||||||
|
action: ${{ matrix.action }}
|
||||||
|
|
||||||
check-rust:
|
check-rust:
|
||||||
if: always()
|
if: always()
|
||||||
needs:
|
needs:
|
||||||
@ -208,7 +240,9 @@ jobs:
|
|||||||
- check-gitbutler-core
|
- check-gitbutler-core
|
||||||
- check-gitbutler-changeset
|
- check-gitbutler-changeset
|
||||||
- check-gitbutler-git
|
- check-gitbutler-git
|
||||||
|
- check-gitbutler-cli
|
||||||
- check-rust-windows
|
- check-rust-windows
|
||||||
|
- rust-lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Decide whether the needed jobs succeeded or failed
|
- name: Decide whether the needed jobs succeeded or failed
|
||||||
@ -223,6 +257,6 @@ jobs:
|
|||||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: ./.github/actions/init-env-rust
|
||||||
- name: "cargo check"
|
- name: "cargo check"
|
||||||
run: cargo check --all --bins --examples
|
run: cargo check --all --bins --examples --features windows
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,3 +7,6 @@
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
470
Cargo.lock
generated
470
Cargo.lock
generated
@ -97,14 +97,60 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anstream"
|
||||||
version = "1.0.81"
|
version = "0.6.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
@ -300,9 +346,9 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.79"
|
version = "0.1.80"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681"
|
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -434,6 +480,12 @@ version = "0.21.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@ -752,9 +804,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.37"
|
version = "0.4.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
|
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
@ -775,6 +827,33 @@ dependencies = [
|
|||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim 0.11.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clru"
|
name = "clru"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@ -817,6 +896,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.6"
|
version = "4.6.6"
|
||||||
@ -1095,7 +1180,7 @@ dependencies = [
|
|||||||
"ident_case",
|
"ident_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim 0.10.0",
|
||||||
"syn 2.0.58",
|
"syn 2.0.58",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1123,16 +1208,6 @@ dependencies = [
|
|||||||
"parking_lot_core 0.9.9",
|
"parking_lot_core 0.9.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "debugid"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
@ -1415,6 +1490,17 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.2.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||||
|
dependencies = [
|
||||||
|
"errno-dragonfly",
|
||||||
|
"libc",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@ -1425,6 +1511,16 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno-dragonfly"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "2.5.3"
|
version = "2.5.3"
|
||||||
@ -1598,18 +1694,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "findshlibs"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@ -2031,21 +2115,6 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gitbutler-analytics"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"chrono",
|
|
||||||
"gitbutler-core",
|
|
||||||
"reqwest 0.12.2",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gitbutler-changeset"
|
name = "gitbutler-changeset"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@ -2054,6 +2123,17 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gitbutler-cli"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"gitbutler-core",
|
||||||
|
"pager",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gitbutler-core"
|
name = "gitbutler-core"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@ -2084,7 +2164,7 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"refinery",
|
"refinery",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.2",
|
"reqwest 0.12.4",
|
||||||
"resolve-path",
|
"resolve-path",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2094,6 +2174,7 @@ dependencies = [
|
|||||||
"slug",
|
"slug",
|
||||||
"ssh-key",
|
"ssh-key",
|
||||||
"ssh2",
|
"ssh2",
|
||||||
|
"strum",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -2131,7 +2212,6 @@ dependencies = [
|
|||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
"futures",
|
"futures",
|
||||||
"git2",
|
"git2",
|
||||||
"gitbutler-analytics",
|
|
||||||
"gitbutler-core",
|
"gitbutler-core",
|
||||||
"gitbutler-testsupport",
|
"gitbutler-testsupport",
|
||||||
"gitbutler-watcher",
|
"gitbutler-watcher",
|
||||||
@ -2140,9 +2220,7 @@ dependencies = [
|
|||||||
"nonzero_ext",
|
"nonzero_ext",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"reqwest 0.12.2",
|
"reqwest 0.12.4",
|
||||||
"sentry",
|
|
||||||
"sentry-tracing",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"slug",
|
"slug",
|
||||||
@ -2182,7 +2260,6 @@ dependencies = [
|
|||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"futures",
|
"futures",
|
||||||
"git2",
|
"git2",
|
||||||
"gitbutler-analytics",
|
|
||||||
"gitbutler-core",
|
"gitbutler-core",
|
||||||
"gitbutler-testsupport",
|
"gitbutler-testsupport",
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
@ -3117,6 +3194,12 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@ -3147,17 +3230,6 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hostname"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"match_cfg",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.26.0"
|
version = "0.26.0"
|
||||||
@ -3526,6 +3598,12 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -3860,12 +3938,6 @@ dependencies = [
|
|||||||
"tendril",
|
"tendril",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "match_cfg"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -4399,6 +4471,16 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pager"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880"
|
||||||
|
dependencies = [
|
||||||
|
"errno 0.2.8",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.15.10"
|
version = "0.15.10"
|
||||||
@ -5291,7 +5373,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile 1.0.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@ -5311,11 +5393,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.2"
|
version = "0.12.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338"
|
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -5335,7 +5417,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile 2.1.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@ -5348,7 +5430,7 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"winreg 0.50.0",
|
"winreg 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5496,7 +5578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"errno",
|
"errno 0.3.8",
|
||||||
"io-lifetimes",
|
"io-lifetimes",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.3.8",
|
"linux-raw-sys 0.3.8",
|
||||||
@ -5510,7 +5592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"errno",
|
"errno 0.3.8",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.13",
|
"linux-raw-sys 0.4.13",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
@ -5525,6 +5607,22 @@ dependencies = [
|
|||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pemfile"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
@ -5648,140 +5746,20 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "766448f12e44d68e675d5789a261515c46ac6ccd240abdd451a9c46c84a49523"
|
|
||||||
dependencies = [
|
|
||||||
"httpdate",
|
|
||||||
"native-tls",
|
|
||||||
"reqwest 0.11.27",
|
|
||||||
"sentry-anyhow",
|
|
||||||
"sentry-backtrace",
|
|
||||||
"sentry-contexts",
|
|
||||||
"sentry-core",
|
|
||||||
"sentry-debug-images",
|
|
||||||
"sentry-panic",
|
|
||||||
"sentry-tracing",
|
|
||||||
"tokio",
|
|
||||||
"ureq",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-anyhow"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4da4015667c99f88d68ca7ff02b90c762d6154a4ceb7c02922b9a1dbd3959eeb"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"sentry-backtrace",
|
|
||||||
"sentry-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-backtrace"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32701cad8b3c78101e1cd33039303154791b0ff22e7802ed8cc23212ef478b45"
|
|
||||||
dependencies = [
|
|
||||||
"backtrace",
|
|
||||||
"once_cell",
|
|
||||||
"regex",
|
|
||||||
"sentry-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-contexts"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "17ddd2a91a13805bd8dab4ebf47323426f758c35f7bf24eacc1aded9668f3824"
|
|
||||||
dependencies = [
|
|
||||||
"hostname",
|
|
||||||
"libc",
|
|
||||||
"os_info",
|
|
||||||
"rustc_version",
|
|
||||||
"sentry-core",
|
|
||||||
"uname",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-core"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b1189f68d7e7e102ef7171adf75f83a59607fafd1a5eecc9dc06c026ff3bdec4"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"sentry-types",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-debug-images"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7b4d0a615e5eeca5699030620c119a094e04c14cf6b486ea1030460a544111a7"
|
|
||||||
dependencies = [
|
|
||||||
"findshlibs",
|
|
||||||
"once_cell",
|
|
||||||
"sentry-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-panic"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d1c18d0b5fba195a4950f2f4c31023725c76f00aabb5840b7950479ece21b5ca"
|
|
||||||
dependencies = [
|
|
||||||
"sentry-backtrace",
|
|
||||||
"sentry-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-tracing"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3012699a9957d7f97047fd75d116e22d120668327db6e7c59824582e16e791b2"
|
|
||||||
dependencies = [
|
|
||||||
"sentry-backtrace",
|
|
||||||
"sentry-core",
|
|
||||||
"tracing-core",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sentry-types"
|
|
||||||
version = "0.32.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c7173fd594569091f68a7c37a886e202f4d0c1db1e1fa1d18a051ba695b2e2ec"
|
|
||||||
dependencies = [
|
|
||||||
"debugid",
|
|
||||||
"hex",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"time",
|
|
||||||
"url",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.197"
|
version = "1.0.199"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.197"
|
version = "1.0.199"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -5790,9 +5768,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.115"
|
version = "1.0.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
|
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.2.6",
|
"indexmap 2.2.6",
|
||||||
"itoa 1.0.11",
|
"itoa 1.0.11",
|
||||||
@ -6129,9 +6107,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ssh-key"
|
name = "ssh-key"
|
||||||
version = "0.6.5"
|
version = "0.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b71299a724c8d84956caaf8fc3b3ea57c3587fe2d0b800cd0dc1f3599905d7e"
|
checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"p256",
|
"p256",
|
||||||
@ -6213,6 +6191,34 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.4.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn 2.0.58",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -6274,9 +6280,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysinfo"
|
name = "sysinfo"
|
||||||
version = "0.30.8"
|
version = "0.30.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b1a378e48fb3ce3a5cf04359c456c9c98ff689bcf1c1bc6e6a31f247686f275"
|
checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
@ -6417,9 +6423,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "1.6.1"
|
version = "1.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f078117725e36d55d29fafcbb4b1e909073807ca328ae8deb8c0b3843aac0fed"
|
checksum = "047aefcc7721bfb8024a9bc39d4719112262610502de7a224fa62c4570cd78d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@ -6433,7 +6439,7 @@ dependencies = [
|
|||||||
"glib",
|
"glib",
|
||||||
"glob",
|
"glob",
|
||||||
"gtk",
|
"gtk",
|
||||||
"heck 0.4.1",
|
"heck 0.5.0",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"ignore",
|
"ignore",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
@ -6551,7 +6557,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-log"
|
name = "tauri-plugin-log"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#773b4983928831b90c16304b51d2fdded9d75066"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byte-unit",
|
"byte-unit",
|
||||||
"fern",
|
"fern",
|
||||||
@ -6566,7 +6572,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-single-instance"
|
name = "tauri-plugin-single-instance"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#773b4983928831b90c16304b51d2fdded9d75066"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@ -6580,7 +6586,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-store"
|
name = "tauri-plugin-store"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#773b4983928831b90c16304b51d2fdded9d75066"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@ -6592,7 +6598,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-window-state"
|
name = "tauri-plugin-window-state"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#773b4983928831b90c16304b51d2fdded9d75066"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#5e3900e682e13f3759b439116ae2f77a6d389ca2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
@ -6715,18 +6721,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.58"
|
version = "1.0.59"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.58"
|
version = "1.0.59"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -7122,15 +7128,6 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "uname"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@ -7164,19 +7161,6 @@ version = "1.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ureq"
|
|
||||||
version = "2.9.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.21.7",
|
|
||||||
"log",
|
|
||||||
"native-tls",
|
|
||||||
"once_cell",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -7207,6 +7191,12 @@ version = "0.1.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/gitbutler-analytics",
|
|
||||||
"crates/gitbutler-core",
|
"crates/gitbutler-core",
|
||||||
"crates/gitbutler-tauri",
|
"crates/gitbutler-tauri",
|
||||||
"crates/gitbutler-changeset",
|
"crates/gitbutler-changeset",
|
||||||
"crates/gitbutler-git",
|
"crates/gitbutler-git",
|
||||||
"crates/gitbutler-watcher",
|
"crates/gitbutler-watcher",
|
||||||
"crates/gitbutler-testsupport",
|
"crates/gitbutler-testsupport",
|
||||||
|
"crates/gitbutler-cli",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@ -15,18 +15,18 @@ gix = { version = "0.62.0", default-features = false, features = [] } # add perf
|
|||||||
git2 = { version = "0.18.3", features = ["vendored-openssl", "vendored-libgit2"] }
|
git2 = { version = "0.18.3", features = ["vendored-openssl", "vendored-libgit2"] }
|
||||||
uuid = { version = "1.8.0", features = ["serde"] }
|
uuid = { version = "1.8.0", features = ["serde"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
thiserror = "1.0.58"
|
thiserror = "1.0.59"
|
||||||
rusqlite = { version = "0.29.0", features = [ "bundled", "blob" ] }
|
rusqlite = { version = "0.29.0", features = [ "bundled", "blob" ] }
|
||||||
tokio = { version = "1.37.0", default-features = false }
|
tokio = { version = "1.37.0", default-features = false }
|
||||||
|
|
||||||
gitbutler-git = { path = "crates/gitbutler-git" }
|
gitbutler-git = { path = "crates/gitbutler-git" }
|
||||||
gitbutler-core = { path = "crates/gitbutler-core" }
|
gitbutler-core = { path = "crates/gitbutler-core" }
|
||||||
gitbutler-analytics = { path = "crates/gitbutler-analytics" }
|
|
||||||
gitbutler-watcher = { path = "crates/gitbutler-watcher" }
|
gitbutler-watcher = { path = "crates/gitbutler-watcher" }
|
||||||
gitbutler-testsupport = { path = "crates/gitbutler-testsupport" }
|
gitbutler-testsupport = { path = "crates/gitbutler-testsupport" }
|
||||||
|
gitbutler-cli ={ path = "crates/gitbutler-cli" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||||
lto = true # Enables link to optimizations
|
lto = true # Enables link to optimizations
|
||||||
opt-level = "s" # Optimize for binary size
|
opt-level = "s" # Optimize for binary size
|
||||||
debug = true # Enable debug symbols, for sentry
|
debug = true # Enable debug symbols, for profiling
|
||||||
|
@ -168,29 +168,21 @@ We use `pnpm`, which requires a relatively recent version of Node.js.
|
|||||||
Make sure that the latest stable version of Node.js is installed and
|
Make sure that the latest stable version of Node.js is installed and
|
||||||
on the PATH, and then `npm i -g pnpm`.
|
on the PATH, and then `npm i -g pnpm`.
|
||||||
|
|
||||||
This often causes file permissions. First, the AppData folder may not
|
Sometimes npm's prefix is incorrect on Windows, we can check this via:
|
||||||
be present. Be sure to create it if it isn't.
|
|
||||||
|
|
||||||
```
|
```sh
|
||||||
mkdir %APPDATA%\npm
|
npm config get prefix
|
||||||
```
|
```
|
||||||
|
|
||||||
Secondly, typically folders within `Program Files` are not writable.
|
If it's not `C:\Users\<username>\AppData\Roaming\npm` or another folder that is
|
||||||
You'll need to fix the security permissions for the `nodejs` folder.
|
normally writable, then we can set it in Powershell:
|
||||||
|
|
||||||
> **NOTE:** Under specific circumstances, depending on your usage of
|
```sh
|
||||||
> Node.js, this may pose a security concern. Be sure to understand
|
mkdir -p $APPDATA\npm
|
||||||
> the implications of this before proceeding.
|
npm config set prefix $env:APPDATA\npm
|
||||||
|
```
|
||||||
|
|
||||||
1. Right-click on the `nodejs` folder in `Program Files`.
|
Afterwards, add this folder to your PATH.
|
||||||
2. Click on `Properties`.
|
|
||||||
3. Click on the `Security` tab.
|
|
||||||
4. Click on `Edit` next to "change permissions".
|
|
||||||
5. Click on `Add`.
|
|
||||||
6. Type in the name of your user account, or type `Everyone` (case-sensitive).
|
|
||||||
Click `Check Names` to verify (they will be underlined if correct).
|
|
||||||
7. Make sure that `Full Control` is checked under `Allow`.
|
|
||||||
8. Apply / click OK as needed to close the dialogs.
|
|
||||||
|
|
||||||
### Perl
|
### Perl
|
||||||
|
|
||||||
@ -198,6 +190,7 @@ A Perl interpreter is required to be installed in order to configure the `openss
|
|||||||
crate. We've used [Strawberry Perl](https://strawberryperl.com/) without issue.
|
crate. We've used [Strawberry Perl](https://strawberryperl.com/) without issue.
|
||||||
Make sure it's installed and `perl` is available on the `PATH` (it is by default
|
Make sure it's installed and `perl` is available on the `PATH` (it is by default
|
||||||
after installation, just make sure to restart the terminal after installing).
|
after installation, just make sure to restart the terminal after installing).
|
||||||
|
[Scoop](https://scoop.sh/) users can install this via `scoop install perl`.
|
||||||
|
|
||||||
Note that it might appear that the build has hung or frozen on the `openssl-sys` crate.
|
Note that it might appear that the build has hung or frozen on the `openssl-sys` crate.
|
||||||
It's not, it's just that Cargo can't report the status of a C/C++ build happening
|
It's not, it's just that Cargo can't report the status of a C/C++ build happening
|
||||||
|
@ -40,17 +40,17 @@
|
|||||||
"@lezer/highlight": "^1.2.0",
|
"@lezer/highlight": "^1.2.0",
|
||||||
"@octokit/rest": "^20.1.0",
|
"@octokit/rest": "^20.1.0",
|
||||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||||
"@sentry/sveltekit": "^7.111.0",
|
"@sentry/sveltekit": "^7.112.2",
|
||||||
"@sveltejs/adapter-static": "^2.0.3",
|
"@sveltejs/adapter-static": "^2.0.3",
|
||||||
"@sveltejs/kit": "^1.30.4",
|
"@sveltejs/kit": "^1.30.4",
|
||||||
"@tauri-apps/api": "^1.5.3",
|
"@tauri-apps/api": "^1.5.4",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/diff": "^5.2.0",
|
"@types/diff": "^5.2.0",
|
||||||
"@types/diff-match-patch": "^1.0.36",
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
"@types/lscache": "^1.3.4",
|
"@types/lscache": "^1.3.4",
|
||||||
"@types/marked": "^5.0.2",
|
"@types/marked": "^5.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||||
"@typescript-eslint/parser": "^7.7.0",
|
"@typescript-eslint/parser": "^7.7.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
@ -61,7 +61,7 @@
|
|||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-square-svelte-store": "^1.0.0",
|
"eslint-plugin-square-svelte-store": "^1.0.0",
|
||||||
"eslint-plugin-svelte": "^2.37.0",
|
"eslint-plugin-svelte": "^2.38.0",
|
||||||
"inter-ui": "^4.0.2",
|
"inter-ui": "^4.0.2",
|
||||||
"leven": "^4.0.0",
|
"leven": "^4.0.0",
|
||||||
"lscache": "^1.3.2",
|
"lscache": "^1.3.2",
|
||||||
@ -71,14 +71,14 @@
|
|||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"postcss-load-config": "^5.0.3",
|
"postcss-load-config": "^5.0.3",
|
||||||
"posthog-js": "1.128.2",
|
"posthog-js": "1.130.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-svelte": "^3.2.3",
|
"prettier-plugin-svelte": "^3.2.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"svelte": "^4.2.15",
|
"svelte": "^4.2.15",
|
||||||
"svelte-check": "^3.6.9",
|
"svelte-check": "^3.7.0",
|
||||||
"svelte-floating-ui": "^1.5.8",
|
"svelte-floating-ui": "^1.5.8",
|
||||||
"svelte-french-toast": "^1.2.0",
|
"svelte-french-toast": "^1.2.0",
|
||||||
"svelte-loadable-store": "^2.0.1",
|
"svelte-loadable-store": "^2.0.1",
|
||||||
@ -95,6 +95,6 @@
|
|||||||
"vitest": "^0.34.6"
|
"vitest": "^0.34.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"openai": "^4.38.2"
|
"openai": "^4.38.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
app/src/global.d.ts
vendored
9
app/src/global.d.ts
vendored
@ -1,9 +0,0 @@
|
|||||||
declare type Item = import('svelte-dnd-action').Item;
|
|
||||||
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action/typings').DndEvent<ItemType>;
|
|
||||||
declare namespace svelte.JSX {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
interface HTMLAttributes<T> {
|
|
||||||
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void;
|
|
||||||
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void;
|
|
||||||
}
|
|
||||||
}
|
|
34
app/src/lib/analytics/analytics.ts
Normal file
34
app/src/lib/analytics/analytics.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { initPostHog } from '$lib/analytics/posthog';
|
||||||
|
import { initSentry } from '$lib/analytics/sentry';
|
||||||
|
import { appAnalyticsConfirmed } from '$lib/config/appSettings';
|
||||||
|
import {
|
||||||
|
appMetricsEnabled,
|
||||||
|
appErrorReportingEnabled,
|
||||||
|
appNonAnonMetricsEnabled
|
||||||
|
} from '$lib/config/appSettings';
|
||||||
|
import posthog from 'posthog-js';
|
||||||
|
|
||||||
|
export function initAnalyticsIfEnabled() {
|
||||||
|
const analyticsConfirmed = appAnalyticsConfirmed();
|
||||||
|
analyticsConfirmed.onDisk().then((confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
appErrorReportingEnabled()
|
||||||
|
.onDisk()
|
||||||
|
.then((enabled) => {
|
||||||
|
if (enabled) initSentry();
|
||||||
|
});
|
||||||
|
appMetricsEnabled()
|
||||||
|
.onDisk()
|
||||||
|
.then((enabled) => {
|
||||||
|
if (enabled) initPostHog();
|
||||||
|
});
|
||||||
|
appNonAnonMetricsEnabled()
|
||||||
|
.onDisk()
|
||||||
|
.then((enabled) => {
|
||||||
|
enabled
|
||||||
|
? posthog.capture('nonAnonMetricsEnabled')
|
||||||
|
: posthog.capture('nonAnonMetricsDisabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -7,7 +7,7 @@ export async function initPostHog() {
|
|||||||
const [appName, appVersion] = await Promise.all([getName(), getVersion()]);
|
const [appName, appVersion] = await Promise.all([getName(), getVersion()]);
|
||||||
posthog.init(PUBLIC_POSTHOG_API_KEY, {
|
posthog.init(PUBLIC_POSTHOG_API_KEY, {
|
||||||
api_host: 'https://eu.posthog.com',
|
api_host: 'https://eu.posthog.com',
|
||||||
disable_session_recording: appName !== 'GitButler', // only record sessions in production
|
disable_session_recording: true,
|
||||||
capture_performance: false,
|
capture_performance: false,
|
||||||
request_batching: true,
|
request_batching: true,
|
||||||
persistence: 'localStorage',
|
persistence: 'localStorage',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AnalyticsSettings from './AnalyticsSettings.svelte';
|
import AnalyticsSettings from './AnalyticsSettings.svelte';
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
|
import { initAnalyticsIfEnabled } from '$lib/analytics/analytics';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
export let analyticsConfirmed: Writable<boolean>;
|
export let analyticsConfirmed: Writable<boolean>;
|
||||||
@ -17,6 +18,7 @@
|
|||||||
icon="chevron-right-small"
|
icon="chevron-right-small"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$analyticsConfirmed = true;
|
$analyticsConfirmed = true;
|
||||||
|
initAnalyticsIfEnabled();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import InfoMessage from './InfoMessage.svelte';
|
|
||||||
import Link from './Link.svelte';
|
import Link from './Link.svelte';
|
||||||
import SectionCard from './SectionCard.svelte';
|
import SectionCard from './SectionCard.svelte';
|
||||||
import Toggle from './Toggle.svelte';
|
import Toggle from './Toggle.svelte';
|
||||||
import { appErrorReportingEnabled, appMetricsEnabled } from '$lib/config/appSettings';
|
import {
|
||||||
|
appErrorReportingEnabled,
|
||||||
|
appMetricsEnabled,
|
||||||
|
appNonAnonMetricsEnabled
|
||||||
|
} from '$lib/config/appSettings';
|
||||||
|
|
||||||
const errorReportingEnabled = appErrorReportingEnabled();
|
const errorReportingEnabled = appErrorReportingEnabled();
|
||||||
const metricsEnabled = appMetricsEnabled();
|
const metricsEnabled = appMetricsEnabled();
|
||||||
let updatedTelemetrySettings = false;
|
const nonAnonMetricsEnabled = appNonAnonMetricsEnabled();
|
||||||
|
|
||||||
function toggleErrorReporting() {
|
function toggleErrorReporting() {
|
||||||
$errorReportingEnabled = !$errorReportingEnabled;
|
$errorReportingEnabled = !$errorReportingEnabled;
|
||||||
updatedTelemetrySettings = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMetrics() {
|
function toggleMetrics() {
|
||||||
$metricsEnabled = !$metricsEnabled;
|
$metricsEnabled = !$metricsEnabled;
|
||||||
updatedTelemetrySettings = true;
|
}
|
||||||
|
|
||||||
|
function toggleNonAnonMetrics() {
|
||||||
|
$nonAnonMetricsEnabled = !$nonAnonMetricsEnabled;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -24,7 +29,13 @@
|
|||||||
<div class="analytics-settings__content">
|
<div class="analytics-settings__content">
|
||||||
<p class="text-base-body-13 analytics-settings__text">
|
<p class="text-base-body-13 analytics-settings__text">
|
||||||
GitButler uses telemetry strictly to help us improve the client. We do not collect any
|
GitButler uses telemetry strictly to help us improve the client. We do not collect any
|
||||||
personal information.
|
personal information (<Link
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="https://gitbutler.com/privacy"
|
||||||
|
>
|
||||||
|
privacy policy
|
||||||
|
</Link>).
|
||||||
</p>
|
</p>
|
||||||
<p class="text-base-body-13 analytics-settings__text">
|
<p class="text-base-body-13 analytics-settings__text">
|
||||||
We kindly ask you to consider keeping these settings enabled as it helps us catch issues more
|
We kindly ask you to consider keeping these settings enabled as it helps us catch issues more
|
||||||
@ -61,13 +72,19 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
{#if updatedTelemetrySettings}
|
<SectionCard labelFor="nonAnonMetricsEnabledToggle" on:click={toggleMetrics} orientation="row">
|
||||||
<InfoMessage>
|
<svelte:fragment slot="title">Non-anonymous usage metrics</svelte:fragment>
|
||||||
<svelte:fragment slot="content"
|
<svelte:fragment slot="caption"
|
||||||
>Changes will take effect on the next application start.</svelte:fragment
|
>Toggle sharing of identifiable usage statistics.</svelte:fragment
|
||||||
>
|
>
|
||||||
</InfoMessage>
|
<svelte:fragment slot="actions">
|
||||||
{/if}
|
<Toggle
|
||||||
|
id="nonAnonMetricsEnabledToggle"
|
||||||
|
checked={$nonAnonMetricsEnabled}
|
||||||
|
on:change={toggleNonAnonMetrics}
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
|
</SectionCard>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
export let help = '';
|
export let help = '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="badge text-base-10 text-semibold" use:tooltip={help}>
|
<div class="badge text-base-10 text-bold" use:tooltip={help}>
|
||||||
{count}
|
{count}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -15,12 +15,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
height: var(--size-16);
|
height: var(--size-14);
|
||||||
min-width: var(--size-16);
|
min-width: var(--size-14);
|
||||||
border-radius: var(--size-16);
|
border-radius: var(--size-14);
|
||||||
padding: 0 var(--size-4);
|
padding: 0 var(--size-4);
|
||||||
color: var(--clr-scale-ntrl-100);
|
color: var(--clr-scale-ntrl-100);
|
||||||
background-color: var(--clr-scale-ntrl-50);
|
background-color: var(--clr-scale-ntrl-40);
|
||||||
line-height: 90%;
|
line-height: 90%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<div class="commits-list">
|
<div class="commits-list">
|
||||||
{#each base.upstreamCommits as commit}
|
{#each base.upstreamCommits as commit}
|
||||||
<CommitCard {commit} commitUrl={base.commitUrl(commit.id)} />
|
<CommitCard {commit} isUnapplied={true} commitUrl={base.commitUrl(commit.id)} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<Spacer margin={2} />
|
<Spacer margin={2} />
|
||||||
@ -62,7 +62,7 @@
|
|||||||
Local
|
Local
|
||||||
</h1>
|
</h1>
|
||||||
{#each base.recentCommits as commit}
|
{#each base.recentCommits as commit}
|
||||||
<CommitCard {commit} commitUrl={base.commitUrl(commit.id)} />
|
<CommitCard {commit} isUnapplied={true} commitUrl={base.commitUrl(commit.id)} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
.row_1 {
|
.row_1 {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--size-6);
|
gap: var(--size-4);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--clr-scale-ntrl-10);
|
color: var(--clr-scale-ntrl-10);
|
||||||
}
|
}
|
||||||
|
110
app/src/lib/components/BaseBranchSwitch.svelte
Normal file
110
app/src/lib/components/BaseBranchSwitch.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
import InfoMessage from './InfoMessage.svelte';
|
||||||
|
import Select from './Select.svelte';
|
||||||
|
import { Project } from '$lib/backend/projects';
|
||||||
|
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||||
|
import SelectItem from '$lib/components/SelectItem.svelte';
|
||||||
|
import Spacer from '$lib/components/Spacer.svelte';
|
||||||
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
|
import { getRemoteBranches } from '$lib/vbranches/baseBranch';
|
||||||
|
import { BranchController } from '$lib/vbranches/branchController';
|
||||||
|
import { BaseBranch } from '$lib/vbranches/types';
|
||||||
|
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||||
|
|
||||||
|
const baseBranch = getContextStore(BaseBranch);
|
||||||
|
const vbranchService = getContext(VirtualBranchService);
|
||||||
|
const branchController = getContext(BranchController);
|
||||||
|
const activeBranches = vbranchService.activeBranches;
|
||||||
|
|
||||||
|
let project = getContext(Project);
|
||||||
|
|
||||||
|
let selectedBranch: {
|
||||||
|
name: string;
|
||||||
|
} = {
|
||||||
|
name: $baseBranch.branchName
|
||||||
|
};
|
||||||
|
let isSwitching = false;
|
||||||
|
|
||||||
|
async function onSetBaseBranchClick() {
|
||||||
|
if (!selectedBranch) return;
|
||||||
|
|
||||||
|
// while target is setting, display loading
|
||||||
|
isSwitching = true;
|
||||||
|
|
||||||
|
await branchController
|
||||||
|
.setTarget(selectedBranch.name)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('error', err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isSwitching = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: console.log('selectedBranch', selectedBranch);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $activeBranches}
|
||||||
|
<SectionCard>
|
||||||
|
<svelte:fragment slot="title">Current base branch</svelte:fragment>
|
||||||
|
<form class="form-wrapper">
|
||||||
|
{#await getRemoteBranches(project.id)}
|
||||||
|
loading remote branches...
|
||||||
|
{:then remoteBranches}
|
||||||
|
<div class="fields-wrapper">
|
||||||
|
<Select
|
||||||
|
items={remoteBranches}
|
||||||
|
bind:value={selectedBranch}
|
||||||
|
itemId="name"
|
||||||
|
labelId="name"
|
||||||
|
selectedItemId={$baseBranch.branchName}
|
||||||
|
wide
|
||||||
|
disabled={$activeBranches.length > 0}
|
||||||
|
>
|
||||||
|
<SelectItem slot="template" let:item let:selected {selected}>
|
||||||
|
{item.name}
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="cta"
|
||||||
|
style="ghost"
|
||||||
|
kind="solid"
|
||||||
|
on:click={onSetBaseBranchClick}
|
||||||
|
id="set-base-branch"
|
||||||
|
loading={isSwitching}
|
||||||
|
disabled={selectedBranch.name === $baseBranch.branchName}
|
||||||
|
>
|
||||||
|
Switch branch
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $activeBranches.length > 0}
|
||||||
|
<InfoMessage filled outlined={false}>
|
||||||
|
<svelte:fragment slot="content">
|
||||||
|
You have {$activeBranches.length === 1
|
||||||
|
? '1 active branch'
|
||||||
|
: `${$activeBranches.length} active branches`} in your workspace. Please clear the workspace
|
||||||
|
before switching the base branch.
|
||||||
|
</svelte:fragment>
|
||||||
|
</InfoMessage>
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
|
</form>
|
||||||
|
</SectionCard>
|
||||||
|
<Spacer />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fields-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-16);
|
||||||
|
}
|
||||||
|
</style>
|
@ -16,7 +16,6 @@
|
|||||||
const branchController = getContext(BranchController);
|
const branchController = getContext(BranchController);
|
||||||
const baseBranch = getContextStore(BaseBranch);
|
const baseBranch = getContextStore(BaseBranch);
|
||||||
const project = getContext(Project);
|
const project = getContext(Project);
|
||||||
|
|
||||||
const activeBranchesError = vbranchService.activeBranchesError;
|
const activeBranchesError = vbranchService.activeBranchesError;
|
||||||
const activeBranches = vbranchService.activeBranches;
|
const activeBranches = vbranchService.activeBranches;
|
||||||
|
|
||||||
@ -27,12 +26,16 @@
|
|||||||
|
|
||||||
let dragHandle: any;
|
let dragHandle: any;
|
||||||
let clone: any;
|
let clone: any;
|
||||||
|
|
||||||
|
let isSwitching = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $activeBranchesError}
|
{#if $activeBranchesError}
|
||||||
<div class="p-4" data-tauri-drag-region>Something went wrong...</div>
|
<div class="p-4" data-tauri-drag-region>Something went wrong...</div>
|
||||||
{:else if !$activeBranches}
|
{:else if !$activeBranches}
|
||||||
<FullviewLoading />
|
<FullviewLoading />
|
||||||
|
{:else if isSwitching}
|
||||||
|
<div class="middle-message">switching base branch...</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="board"
|
class="board"
|
||||||
@ -202,6 +205,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-16);
|
||||||
|
}
|
||||||
|
|
||||||
.branch {
|
.branch {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -255,6 +264,38 @@
|
|||||||
padding-left: var(--size-4);
|
padding-left: var(--size-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.branch-switcher {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--clr-bg-2);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--clr-border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-message {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 2em;
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-board__image-frame {
|
.empty-board__image-frame {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--clr-theme-container-light);
|
background: var(--clr-bg-1);
|
||||||
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||||
padding: 0 var(--size-14) var(--size-14);
|
padding: 0 var(--size-14) var(--size-14);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
branchName={branch.upstreamName ?? branchName}
|
branchName={branch.upstreamName ?? branchName}
|
||||||
{isUnapplied}
|
{isUnapplied}
|
||||||
{hasIntegratedCommits}
|
{hasIntegratedCommits}
|
||||||
remoteExists={!!branch.upstreamName}
|
remoteExists={!!branch.upstream}
|
||||||
isLaneCollapsed={$isLaneCollapsed}
|
isLaneCollapsed={$isLaneCollapsed}
|
||||||
/>
|
/>
|
||||||
{#if branch.selectedForChanges}
|
{#if branch.selectedForChanges}
|
||||||
@ -118,7 +118,7 @@
|
|||||||
branchName={branch.upstreamName ?? branchName}
|
branchName={branch.upstreamName ?? branchName}
|
||||||
{isUnapplied}
|
{isUnapplied}
|
||||||
{hasIntegratedCommits}
|
{hasIntegratedCommits}
|
||||||
remoteExists={!!branch.upstreamName}
|
remoteExists={!!branch.upstream}
|
||||||
isLaneCollapsed={$isLaneCollapsed}
|
isLaneCollapsed={$isLaneCollapsed}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -61,11 +61,9 @@
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
.branch-name-mesure-el {
|
.branch-name-mesure-el {
|
||||||
pointer-events: auto;
|
pointer-events: none;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
top: -9999px;
|
|
||||||
left: -9999px;
|
|
||||||
color: black;
|
color: black;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -42,6 +42,10 @@
|
|||||||
async function setAIConfigurationValid(user: User | undefined) {
|
async function setAIConfigurationValid(user: User | undefined) {
|
||||||
aiConfigurationValid = await aiService.validateConfiguration(user?.access_token);
|
aiConfigurationValid = await aiService.validateConfiguration(user?.access_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
@ -52,7 +56,7 @@
|
|||||||
label="Unapply"
|
label="Unapply"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (branch.id) branchController.unapplyBranch(branch.id);
|
if (branch.id) branchController.unapplyBranch(branch.id);
|
||||||
visible = false;
|
close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@ -69,7 +73,7 @@
|
|||||||
} else {
|
} else {
|
||||||
deleteBranchModal.show(branch);
|
deleteBranchModal.show(branch);
|
||||||
}
|
}
|
||||||
visible = false;
|
close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -77,7 +81,7 @@
|
|||||||
label="Generate branch name"
|
label="Generate branch name"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dispatch('action', 'generate-branch-name');
|
dispatch('action', 'generate-branch-name');
|
||||||
visible = false;
|
close();
|
||||||
}}
|
}}
|
||||||
disabled={isUnapplied ||
|
disabled={isUnapplied ||
|
||||||
!($aiGenEnabled && aiConfigurationValid) ||
|
!($aiGenEnabled && aiConfigurationValid) ||
|
||||||
@ -91,7 +95,7 @@
|
|||||||
disabled={isUnapplied || hasIntegratedCommits}
|
disabled={isUnapplied || hasIntegratedCommits}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
newRemoteName = branch.upstreamName || normalizeBranchName(branch.name) || '';
|
newRemoteName = branch.upstreamName || normalizeBranchName(branch.name) || '';
|
||||||
visible = false;
|
close();
|
||||||
renameRemoteModal.show(branch);
|
renameRemoteModal.show(branch);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -101,7 +105,7 @@
|
|||||||
label="Create branch to the left"
|
label="Create branch to the left"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
branchController.createBranch({ order: branch.order });
|
branchController.createBranch({ order: branch.order });
|
||||||
visible = false;
|
close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -109,7 +113,7 @@
|
|||||||
label="Create branch to the right"
|
label="Create branch to the right"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
branchController.createBranch({ order: branch.order + 1 });
|
branchController.createBranch({ order: branch.order + 1 });
|
||||||
visible = false;
|
close();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ContextMenuSection>
|
</ContextMenuSection>
|
||||||
@ -122,7 +126,6 @@
|
|||||||
on:submit={() => {
|
on:submit={() => {
|
||||||
branchController.updateBranchRemoteName(branch.id, newRemoteName);
|
branchController.updateBranchRemoteName(branch.id, newRemoteName);
|
||||||
renameRemoteModal.close();
|
renameRemoteModal.close();
|
||||||
visible = false;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svelte:fragment>
|
<svelte:fragment>
|
||||||
@ -136,9 +139,9 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal width="small" title="Delete branch" bind:this={deleteBranchModal} let:item={branch}>
|
<Modal width="small" title="Delete branch" bind:this={deleteBranchModal} let:item={branch}>
|
||||||
<div>
|
<svelte:fragment>
|
||||||
Deleting <code class="code-string">{branch.name}</code> cannot be undone.
|
Deleting <code class="code-string">{branch.name}</code> cannot be undone.
|
||||||
</div>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="controls" let:close let:item={branch}>
|
<svelte:fragment slot="controls" let:close let:item={branch}>
|
||||||
<Button style="ghost" kind="solid" on:click={close}>Cancel</Button>
|
<Button style="ghost" kind="solid" on:click={close}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -146,7 +149,7 @@
|
|||||||
kind="solid"
|
kind="solid"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await branchController.deleteBranch(branch.id);
|
await branchController.deleteBranch(branch.id);
|
||||||
visible = false;
|
deleteBranchModal.close();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
export let id: string | undefined = undefined;
|
export let id: string | undefined = undefined;
|
||||||
export let loading = false;
|
export let loading = false;
|
||||||
export let tabindex: number | undefined = undefined;
|
export let tabindex: number | undefined = undefined;
|
||||||
export let type: 'submit' | 'reset' | undefined = undefined;
|
export let type: 'submit' | 'reset' | 'button' | undefined = undefined;
|
||||||
// Layout props
|
// Layout props
|
||||||
export let reversedDirection: boolean = false;
|
export let reversedDirection: boolean = false;
|
||||||
export let width: number | undefined = undefined;
|
export let width: number | undefined = undefined;
|
||||||
@ -102,39 +102,7 @@
|
|||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.7;
|
opacity: 0.5;
|
||||||
|
|
||||||
&.neutral.solid,
|
|
||||||
&.pop.solid,
|
|
||||||
&.success.solid,
|
|
||||||
&.error.solid,
|
|
||||||
&.warning.solid,
|
|
||||||
&.purple.solid {
|
|
||||||
--btn-clr: var(--clr-text-2);
|
|
||||||
--btn-bg: var(--clr-bg-3);
|
|
||||||
|
|
||||||
& .badge {
|
|
||||||
--btn-bg: var(--clr-scale-ntrl-100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.neutral.soft,
|
|
||||||
&.pop.soft,
|
|
||||||
&.success.soft,
|
|
||||||
&.error.soft,
|
|
||||||
&.warning.soft,
|
|
||||||
&.purple.soft {
|
|
||||||
--btn-clr: var(--clr-text-2);
|
|
||||||
--btn-bg: var(--clr-bg-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ghost {
|
|
||||||
--btn-clr: var(--clr-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ghost.solid {
|
|
||||||
border-color: var(--clr-bg-3);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.wide {
|
&.wide {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -238,7 +206,7 @@
|
|||||||
|
|
||||||
.pop {
|
.pop {
|
||||||
&.soft {
|
&.soft {
|
||||||
--btn-clr: var(--clr-theme-pop-on-container);
|
--btn-clr: var(--clr-theme-pop-on-soft);
|
||||||
--btn-bg: var(--clr-scale-pop-80);
|
--btn-bg: var(--clr-scale-pop-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-clickable, &:disabled):hover {
|
&:not(.not-clickable, &:disabled):hover {
|
||||||
@ -261,7 +229,7 @@
|
|||||||
|
|
||||||
.success {
|
.success {
|
||||||
&.soft {
|
&.soft {
|
||||||
--btn-clr: var(--clr-theme-succ-on-container);
|
--btn-clr: var(--clr-theme-succ-on-soft);
|
||||||
--btn-bg: var(--clr-scale-succ-80);
|
--btn-bg: var(--clr-scale-succ-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-clickable, &:disabled):hover {
|
&:not(.not-clickable, &:disabled):hover {
|
||||||
@ -284,7 +252,7 @@
|
|||||||
|
|
||||||
.error {
|
.error {
|
||||||
&.soft {
|
&.soft {
|
||||||
--btn-clr: var(--clr-theme-err-on-container);
|
--btn-clr: var(--clr-theme-err-on-soft);
|
||||||
--btn-bg: var(--clr-scale-err-80);
|
--btn-bg: var(--clr-scale-err-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-clickable, &:disabled):hover {
|
&:not(.not-clickable, &:disabled):hover {
|
||||||
@ -307,7 +275,7 @@
|
|||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
&.soft {
|
&.soft {
|
||||||
--btn-clr: var(--clr-theme-warn-on-container);
|
--btn-clr: var(--clr-theme-warn-on-soft);
|
||||||
--btn-bg: var(--clr-scale-warn-80);
|
--btn-bg: var(--clr-scale-warn-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-clickable, &:disabled):hover {
|
&:not(.not-clickable, &:disabled):hover {
|
||||||
@ -330,7 +298,7 @@
|
|||||||
|
|
||||||
.purple {
|
.purple {
|
||||||
&.soft {
|
&.soft {
|
||||||
--btn-clr: var(--clr-theme-purp-on-container);
|
--btn-clr: var(--clr-theme-purp-on-soft);
|
||||||
--btn-bg: var(--clr-scale-purp-80);
|
--btn-bg: var(--clr-scale-purp-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-clickable, &:disabled):hover {
|
&:not(.not-clickable, &:disabled):hover {
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BranchFilesList from './BranchFilesList.svelte';
|
import BranchFilesList from './BranchFilesList.svelte';
|
||||||
import { Project } from '$lib/backend/projects';
|
import { Project } from '$lib/backend/projects';
|
||||||
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import CommitMessageInput from '$lib/components/CommitMessageInput.svelte';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import Tag from '$lib/components/Tag.svelte';
|
import Tag from '$lib/components/Tag.svelte';
|
||||||
import TimeAgo from '$lib/components/TimeAgo.svelte';
|
import TimeAgo from '$lib/components/TimeAgo.svelte';
|
||||||
import { persistedCommitMessage } from '$lib/config/config';
|
import { persistedCommitMessage } from '$lib/config/config';
|
||||||
|
import { featureAdvancedCommitOperations } from '$lib/config/uiFeatureFlags';
|
||||||
import { draggable } from '$lib/dragging/draggable';
|
import { draggable } from '$lib/dragging/draggable';
|
||||||
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
||||||
import { getContext, getContextStore } from '$lib/utils/context';
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
@ -26,6 +30,7 @@
|
|||||||
const project = getContext(Project);
|
const project = getContext(Project);
|
||||||
const selectedFiles = getSelectedFiles();
|
const selectedFiles = getSelectedFiles();
|
||||||
const fileIdSelection = getContext(FileIdSelection);
|
const fileIdSelection = getContext(FileIdSelection);
|
||||||
|
const advancedCommitOperations = featureAdvancedCommitOperations();
|
||||||
|
|
||||||
const commitStore = createCommitStore(commit);
|
const commitStore = createCommitStore(commit);
|
||||||
$: commitStore.set(commit);
|
$: commitStore.set(commit);
|
||||||
@ -47,6 +52,7 @@
|
|||||||
|
|
||||||
function toggleFiles() {
|
function toggleFiles() {
|
||||||
showFiles = !showFiles;
|
showFiles = !showFiles;
|
||||||
|
|
||||||
if (showFiles) loadFiles();
|
if (showFiles) loadFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,22 +62,76 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetHeadCommit() {
|
function undoCommit(commit: Commit | RemoteCommit) {
|
||||||
if (!branch || !$baseBranch) {
|
if (!branch || !$baseBranch) {
|
||||||
console.error('Unable to reset head commit');
|
console.error('Unable to undo commit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (branch.commits.length > 1) {
|
branchController.undoCommit(branch.id, commit.id);
|
||||||
branchController.resetBranch(branch.id, branch.commits[1].id);
|
|
||||||
} else if (branch.commits.length === 1 && $baseBranch) {
|
|
||||||
branchController.resetBranch(branch.id, $baseBranch.baseSha);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUndoable = isHeadCommit && !isUnapplied;
|
function insertBlankCommit(commit: Commit | RemoteCommit, offset: number) {
|
||||||
|
if (!branch || !$baseBranch) {
|
||||||
|
console.error('Unable to insert commit');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
branchController.insertBlankCommit(branch.id, commit.id, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorderCommit(commit: Commit | RemoteCommit, offset: number) {
|
||||||
|
if (!branch || !$baseBranch) {
|
||||||
|
console.error('Unable to move commit');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
branchController.reorderCommit(branch.id, commit.id, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isUndoable = false;
|
||||||
|
|
||||||
|
$: if ($advancedCommitOperations) {
|
||||||
|
isUndoable = !!branch?.active && commit instanceof Commit;
|
||||||
|
} else {
|
||||||
|
isUndoable = isHeadCommit;
|
||||||
|
}
|
||||||
const hasCommitUrl = !commit.isLocal && commitUrl;
|
const hasCommitUrl = !commit.isLocal && commitUrl;
|
||||||
|
|
||||||
|
let commitMessageModal: Modal;
|
||||||
|
let commitMessageValid = false;
|
||||||
|
let description = '';
|
||||||
|
|
||||||
|
function openCommitMessageModal(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
description = commit.description;
|
||||||
|
|
||||||
|
commitMessageModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitCommitMessageModal() {
|
||||||
|
commit.description = description;
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
branchController.updateCommitMessage(branch.id, commit.id, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
commitMessageModal.close();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={commitMessageModal}>
|
||||||
|
<CommitMessageInput bind:commitMessage={description} bind:valid={commitMessageValid} />
|
||||||
|
<svelte:fragment slot="controls">
|
||||||
|
<Button style="ghost" kind="solid" on:click={() => commitMessageModal.close()}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
style="pop"
|
||||||
|
kind="solid"
|
||||||
|
grow
|
||||||
|
disabled={!commitMessageValid}
|
||||||
|
on:click={submitCommitMessageModal}>Submit</Button
|
||||||
|
>
|
||||||
|
</svelte:fragment>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
use:draggable={commit instanceof Commit
|
use:draggable={commit instanceof Commit
|
||||||
? {
|
? {
|
||||||
@ -83,11 +143,28 @@
|
|||||||
>
|
>
|
||||||
<div class="commit__header" on:click={toggleFiles} on:keyup={onKeyup} role="button" tabindex="0">
|
<div class="commit__header" on:click={toggleFiles} on:keyup={onKeyup} role="button" tabindex="0">
|
||||||
<div class="commit__message">
|
<div class="commit__message">
|
||||||
|
{#if $advancedCommitOperations}
|
||||||
|
{#if !showFiles}
|
||||||
|
<div class="commit__id">
|
||||||
|
<code>{commit.id.substring(0, 6)}</code>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<div class="commit__row">
|
<div class="commit__row">
|
||||||
|
{#if isUndoable}
|
||||||
|
{#if commit.descriptionTitle}
|
||||||
<span class="commit__title text-semibold text-base-12" class:truncate={!showFiles}>
|
<span class="commit__title text-semibold text-base-12" class:truncate={!showFiles}>
|
||||||
{commit.descriptionTitle}
|
{commit.descriptionTitle}
|
||||||
</span>
|
</span>
|
||||||
{#if isUndoable && !showFiles}
|
{:else}
|
||||||
|
<span
|
||||||
|
class="commit__title_no_desc text-base-12 text-zinc-400"
|
||||||
|
class:truncate={!showFiles}
|
||||||
|
>
|
||||||
|
<i>empty commit message</i>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if !showFiles}
|
||||||
<Tag
|
<Tag
|
||||||
style="ghost"
|
style="ghost"
|
||||||
kind="solid"
|
kind="solid"
|
||||||
@ -96,18 +173,29 @@
|
|||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
currentCommitMessage.set(commit.description);
|
currentCommitMessage.set(commit.description);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
resetHeadCommit();
|
undoCommit(commit);
|
||||||
}}>Undo</Tag
|
}}>Undo</Tag
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<span class="commit__title text-base-12" class:truncate={!showFiles}>
|
||||||
|
{commit.descriptionTitle}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if showFiles && commit.descriptionBody}
|
{#if showFiles}
|
||||||
|
{#if commit.descriptionBody}
|
||||||
<div class="commit__row" transition:slide={{ duration: 100 }}>
|
<div class="commit__row" transition:slide={{ duration: 100 }}>
|
||||||
<span class="commit__body text-base-body-12">
|
<span class="commit__body text-base-body-12">
|
||||||
{commit.descriptionBody}
|
{commit.descriptionBody}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $advancedCommitOperations && isUndoable}
|
||||||
|
<Tag clickable on:click={openCommitMessageModal}>Edit</Tag>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="commit__row">
|
<div class="commit__row">
|
||||||
<div class="commit__author">
|
<div class="commit__author">
|
||||||
@ -130,12 +218,50 @@
|
|||||||
|
|
||||||
{#if showFiles}
|
{#if showFiles}
|
||||||
<div class="files-container" transition:slide={{ duration: 100 }}>
|
<div class="files-container" transition:slide={{ duration: 100 }}>
|
||||||
<BranchFilesList {files} {isUnapplied} readonly />
|
<BranchFilesList {files} {isUnapplied} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if hasCommitUrl || isUndoable}
|
{#if hasCommitUrl || isUndoable}
|
||||||
<div class="files__footer">
|
<div class="files__footer">
|
||||||
{#if isUndoable}
|
{#if isUndoable}
|
||||||
|
{#if $advancedCommitOperations}
|
||||||
|
<Tag
|
||||||
|
style="ghost"
|
||||||
|
kind="solid"
|
||||||
|
clickable
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
reorderCommit(commit, -1);
|
||||||
|
}}>Move Up</Tag
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
style="ghost"
|
||||||
|
kind="solid"
|
||||||
|
clickable
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
reorderCommit(commit, 1);
|
||||||
|
}}>Move Down</Tag
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
style="ghost"
|
||||||
|
kind="solid"
|
||||||
|
clickable
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
insertBlankCommit(commit, -1);
|
||||||
|
}}>Add Before</Tag
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
style="ghost"
|
||||||
|
kind="solid"
|
||||||
|
clickable
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
insertBlankCommit(commit, 1);
|
||||||
|
}}>Add After</Tag
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
<Tag
|
<Tag
|
||||||
style="ghost"
|
style="ghost"
|
||||||
kind="solid"
|
kind="solid"
|
||||||
@ -144,7 +270,7 @@
|
|||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
currentCommitMessage.set(commit.description);
|
currentCommitMessage.set(commit.description);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
resetHeadCommit();
|
undoCommit(commit);
|
||||||
}}>Undo</Tag
|
}}>Undo</Tag
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
@ -221,6 +347,11 @@
|
|||||||
color: var(--clr-scale-ntrl-0);
|
color: var(--clr-scale-ntrl-0);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.commit__title_no_desc {
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.commit__body {
|
.commit__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -237,6 +368,21 @@
|
|||||||
gap: var(--size-8);
|
gap: var(--size-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.commit__id {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: -14px;
|
||||||
|
}
|
||||||
|
.commit__id > code {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
padding: 1px 12px;
|
||||||
|
color: #888888;
|
||||||
|
font-size: x-small;
|
||||||
|
border-radius: 0px 0px 6px 6px;
|
||||||
|
margin-bottom: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
.commit__author {
|
.commit__author {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -268,6 +414,7 @@
|
|||||||
.files__footer {
|
.files__footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: var(--size-8);
|
gap: var(--size-8);
|
||||||
padding: var(--size-14);
|
padding: var(--size-14);
|
||||||
background-color: var(--clr-bg-1);
|
background-color: var(--clr-bg-1);
|
||||||
|
@ -1,80 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
import { AIService } from '$lib/ai/service';
|
import CommitMessageInput from '$lib/components/CommitMessageInput.svelte';
|
||||||
import Checkbox from '$lib/components/Checkbox.svelte';
|
import { projectRunCommitHooks, persistedCommitMessage } from '$lib/config/config';
|
||||||
import DropDownButton from '$lib/components/DropDownButton.svelte';
|
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
|
||||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
|
||||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
|
||||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
|
||||||
import {
|
|
||||||
projectAiGenEnabled,
|
|
||||||
projectCommitGenerationExtraConcise,
|
|
||||||
projectCommitGenerationUseEmojis,
|
|
||||||
projectRunCommitHooks,
|
|
||||||
persistedCommitMessage
|
|
||||||
} from '$lib/config/config';
|
|
||||||
import { showError } from '$lib/notifications/toasts';
|
|
||||||
import { User } from '$lib/stores/user';
|
|
||||||
import { splitMessage } from '$lib/utils/commitMessage';
|
|
||||||
import { getContext, getContextStore } from '$lib/utils/context';
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
import { tooltip } from '$lib/utils/tooltip';
|
|
||||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
|
||||||
import { useResize } from '$lib/utils/useResize';
|
|
||||||
import { BranchController } from '$lib/vbranches/branchController';
|
import { BranchController } from '$lib/vbranches/branchController';
|
||||||
import { Ownership } from '$lib/vbranches/ownership';
|
import { Ownership } from '$lib/vbranches/ownership';
|
||||||
import { Branch, type LocalFile } from '$lib/vbranches/types';
|
import { Branch } from '$lib/vbranches/types';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly, slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
|
|
||||||
const aiService = getContext(AIService);
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
|
||||||
action: 'generate-branch-name';
|
|
||||||
}>();
|
|
||||||
|
|
||||||
export let projectId: string;
|
export let projectId: string;
|
||||||
export let expanded: Writable<boolean>;
|
export let expanded: Writable<boolean>;
|
||||||
|
|
||||||
const branchController = getContext(BranchController);
|
const branchController = getContext(BranchController);
|
||||||
const selectedOwnership = getContextStore(Ownership);
|
const selectedOwnership = getContextStore(Ownership);
|
||||||
const branch = getContextStore(Branch);
|
const branch = getContextStore(Branch);
|
||||||
const user = getContextStore(User);
|
|
||||||
|
|
||||||
const aiGenEnabled = projectAiGenEnabled(projectId);
|
|
||||||
const runCommitHooks = projectRunCommitHooks(projectId);
|
const runCommitHooks = projectRunCommitHooks(projectId);
|
||||||
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
||||||
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(projectId);
|
|
||||||
const commitGenerationUseEmojis = projectCommitGenerationUseEmojis(projectId);
|
|
||||||
|
|
||||||
let isCommitting = false;
|
let isCommitting = false;
|
||||||
let aiLoading = false;
|
|
||||||
|
|
||||||
let contextMenu: ContextMenu;
|
let commitMessageValid = false;
|
||||||
|
|
||||||
let titleTextArea: HTMLTextAreaElement;
|
|
||||||
let descriptionTextArea: HTMLTextAreaElement;
|
|
||||||
|
|
||||||
$: ({ title, description } = splitMessage($commitMessage));
|
|
||||||
$: if ($commitMessage) updateHeights();
|
|
||||||
|
|
||||||
function concatMessage(title: string, description: string) {
|
|
||||||
return `${title}\n\n${description}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusTextareaOnMount(el: HTMLTextAreaElement) {
|
|
||||||
el.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHeights() {
|
|
||||||
useAutoHeight(titleTextArea);
|
|
||||||
useAutoHeight(descriptionTextArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function commit() {
|
async function commit() {
|
||||||
const message = concatMessage(title, description);
|
const message = $commitMessage;
|
||||||
isCommitting = true;
|
isCommitting = true;
|
||||||
try {
|
try {
|
||||||
await branchController.commitBranch(
|
await branchController.commitBranch(
|
||||||
@ -88,158 +39,16 @@
|
|||||||
isCommitting = false;
|
isCommitting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateCommitMessage(files: LocalFile[]) {
|
|
||||||
const hunks = files.flatMap((f) =>
|
|
||||||
f.hunks.filter((h) => $selectedOwnership.contains(f.id, h.id))
|
|
||||||
);
|
|
||||||
// Branches get their names generated only if there are at least 4 lines of code
|
|
||||||
// If the change is a 'one-liner', the branch name is either left as "virtual branch"
|
|
||||||
// or the user has to manually trigger the name generation from the meatball menu
|
|
||||||
// This saves people this extra click
|
|
||||||
if ($branch.name.toLowerCase().includes('virtual branch')) {
|
|
||||||
dispatch('action', 'generate-branch-name');
|
|
||||||
}
|
|
||||||
|
|
||||||
aiLoading = true;
|
|
||||||
try {
|
|
||||||
const generatedMessage = await aiService.summarizeCommit({
|
|
||||||
hunks,
|
|
||||||
useEmojiStyle: $commitGenerationUseEmojis,
|
|
||||||
useBriefStyle: $commitGenerationExtraConcise,
|
|
||||||
userToken: $user?.access_token
|
|
||||||
});
|
|
||||||
|
|
||||||
if (generatedMessage) {
|
|
||||||
$commitMessage = generatedMessage;
|
|
||||||
} else {
|
|
||||||
throw new Error('Prompt generated no response');
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
showError('Failed to generate commit message', e);
|
|
||||||
} finally {
|
|
||||||
aiLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
updateHeights();
|
|
||||||
descriptionTextArea.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let aiConfigurationValid = false;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
aiConfigurationValid = await aiService.validateConfiguration($user?.access_token);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="commit-box" class:commit-box__expanded={$expanded}>
|
<div class="commit-box" class:commit-box__expanded={$expanded}>
|
||||||
{#if $expanded}
|
{#if $expanded}
|
||||||
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
|
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
|
||||||
<div class="commit-box__textarea-wrapper text-input">
|
<CommitMessageInput
|
||||||
<textarea
|
bind:commitMessage={$commitMessage}
|
||||||
value={title}
|
bind:valid={commitMessageValid}
|
||||||
placeholder="Commit summary"
|
{commit}
|
||||||
disabled={aiLoading}
|
|
||||||
class="text-base-body-13 text-semibold commit-box__textarea commit-box__textarea__title"
|
|
||||||
spellcheck="false"
|
|
||||||
rows="1"
|
|
||||||
bind:this={titleTextArea}
|
|
||||||
use:focusTextareaOnMount
|
|
||||||
use:useResize={() => {
|
|
||||||
useAutoHeight(titleTextArea);
|
|
||||||
}}
|
|
||||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
|
||||||
on:input={(e) => {
|
|
||||||
$commitMessage = concatMessage(e.currentTarget.value, description);
|
|
||||||
}}
|
|
||||||
on:keydown={(e) => {
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
|
|
||||||
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
descriptionTextArea.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if title.length > 0 || description}
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
disabled={aiLoading}
|
|
||||||
placeholder="Commit description (optional)"
|
|
||||||
class="text-base-body-13 commit-box__textarea commit-box__textarea__description"
|
|
||||||
spellcheck="false"
|
|
||||||
rows="1"
|
|
||||||
bind:this={descriptionTextArea}
|
|
||||||
use:useResize={() => useAutoHeight(descriptionTextArea)}
|
|
||||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
|
||||||
on:input={(e) => {
|
|
||||||
$commitMessage = concatMessage(title, e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
on:keydown={(e) => {
|
|
||||||
const value = e.currentTarget.value;
|
|
||||||
if (e.key == 'Backspace' && value.length == 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
titleTextArea.focus();
|
|
||||||
useAutoHeight(e.currentTarget);
|
|
||||||
} else if (e.key == 'a' && (e.metaKey || e.ctrlKey) && value.length == 0) {
|
|
||||||
// select previous textarea on cmd+a if this textarea is empty
|
|
||||||
e.preventDefault();
|
|
||||||
titleTextArea.select();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if title.length > 50}
|
|
||||||
<div
|
|
||||||
transition:fly={{ y: 2, duration: 150 }}
|
|
||||||
class="commit-box__textarea-tooltip"
|
|
||||||
use:tooltip={{
|
|
||||||
text: '50 characters or less is best. Extra info can be added in the description.',
|
|
||||||
delay: 200
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="blitz" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="commit-box__texarea-actions"
|
|
||||||
use:tooltip={$aiGenEnabled && aiConfigurationValid
|
|
||||||
? ''
|
|
||||||
: 'You must be logged in or have provided your own API key and have summary generation enabled to use this feature'}
|
|
||||||
>
|
|
||||||
<DropDownButton
|
|
||||||
style="ghost"
|
|
||||||
kind="solid"
|
|
||||||
icon="ai-small"
|
|
||||||
disabled={!($aiGenEnabled && aiConfigurationValid)}
|
|
||||||
loading={aiLoading}
|
|
||||||
on:click={async () => await generateCommitMessage($branch.files)}
|
|
||||||
>
|
|
||||||
Generate message
|
|
||||||
<ContextMenu type="checklist" slot="context-menu" bind:this={contextMenu}>
|
|
||||||
<ContextMenuSection>
|
|
||||||
<ContextMenuItem
|
|
||||||
label="Extra concise"
|
|
||||||
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
|
||||||
>
|
|
||||||
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
|
||||||
</ContextMenuItem>
|
|
||||||
|
|
||||||
<ContextMenuItem
|
|
||||||
label="Use emojis 😎"
|
|
||||||
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
|
||||||
>
|
|
||||||
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuSection>
|
|
||||||
</ContextMenu>
|
|
||||||
</DropDownButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@ -260,7 +69,7 @@
|
|||||||
kind="solid"
|
kind="solid"
|
||||||
grow
|
grow
|
||||||
loading={isCommitting}
|
loading={isCommitting}
|
||||||
disabled={(isCommitting || !title || $selectedOwnership.isEmpty()) && $expanded}
|
disabled={(isCommitting || !commitMessageValid || $selectedOwnership.isEmpty()) && $expanded}
|
||||||
id="commit-to-branch"
|
id="commit-to-branch"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if ($expanded) {
|
if ($expanded) {
|
||||||
@ -292,57 +101,6 @@
|
|||||||
margin-bottom: var(--size-12);
|
margin-bottom: var(--size-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit-box__textarea-wrapper {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
padding: 0 0 var(--size-48);
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--size-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea {
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: var(--size-16);
|
|
||||||
background: none;
|
|
||||||
resize: none;
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: oklch(from var(--clr-scale-ntrl-30) l c h / 0.4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea-tooltip {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
bottom: var(--size-12);
|
|
||||||
left: var(--size-12);
|
|
||||||
padding: var(--size-2);
|
|
||||||
border-radius: 100%;
|
|
||||||
background: var(--clr-bg-2);
|
|
||||||
color: var(--clr-scale-ntrl-40);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea__title {
|
|
||||||
padding: var(--size-12) var(--size-12) 0 var(--size-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__textarea__description {
|
|
||||||
padding: 0 var(--size-12) 0 var(--size-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-box__texarea-actions {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
right: var(--size-12);
|
|
||||||
bottom: var(--size-12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
|
@ -6,8 +6,15 @@
|
|||||||
import { dropzone } from '$lib/dragging/dropzone';
|
import { dropzone } from '$lib/dragging/dropzone';
|
||||||
import { getContext, getContextStore } from '$lib/utils/context';
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
import { BranchController } from '$lib/vbranches/branchController';
|
import { BranchController } from '$lib/vbranches/branchController';
|
||||||
import { filesToOwnership } from '$lib/vbranches/ownership';
|
import { filesToOwnership, filesToSimpleOwnership } from '$lib/vbranches/ownership';
|
||||||
import { RemoteCommit, Branch, type Commit, BaseBranch } from '$lib/vbranches/types';
|
import {
|
||||||
|
RemoteCommit,
|
||||||
|
Branch,
|
||||||
|
type Commit,
|
||||||
|
BaseBranch,
|
||||||
|
LocalFile,
|
||||||
|
RemoteFile
|
||||||
|
} from '$lib/vbranches/types';
|
||||||
|
|
||||||
export let commit: Commit | RemoteCommit;
|
export let commit: Commit | RemoteCommit;
|
||||||
export let isHeadCommit: boolean;
|
export let isHeadCommit: boolean;
|
||||||
@ -32,11 +39,6 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only allow to amend the head commit
|
|
||||||
if (commit.id != $branch.commits.at(0)?.id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data instanceof DraggableHunk && data.branchId == $branch.id) {
|
if (data instanceof DraggableHunk && data.branchId == $branch.id) {
|
||||||
return true;
|
return true;
|
||||||
} else if (data instanceof DraggableFile && data.branchId == $branch.id) {
|
} else if (data instanceof DraggableFile && data.branchId == $branch.id) {
|
||||||
@ -47,15 +49,26 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAmend(data: DraggableFile | DraggableHunk) {
|
function onAmend(commit: Commit | RemoteCommit) {
|
||||||
|
return (data: any) => {
|
||||||
if (data instanceof DraggableHunk) {
|
if (data instanceof DraggableHunk) {
|
||||||
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
|
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
|
||||||
branchController.amendBranch($branch.id, newOwnership);
|
branchController.amendBranch($branch.id, commit.id, newOwnership);
|
||||||
} else if (data instanceof DraggableFile) {
|
} else if (data instanceof DraggableFile) {
|
||||||
|
if (data.file instanceof LocalFile) {
|
||||||
|
// this is an uncommitted file change being amended to a previous commit
|
||||||
const newOwnership = filesToOwnership(data.files);
|
const newOwnership = filesToOwnership(data.files);
|
||||||
branchController.amendBranch($branch.id, newOwnership);
|
branchController.amendBranch($branch.id, commit.id, newOwnership);
|
||||||
|
} else if (data.file instanceof RemoteFile) {
|
||||||
|
// this is a file from a commit, rather than an uncommitted file
|
||||||
|
const newOwnership = filesToSimpleOwnership(data.files);
|
||||||
|
if (data.commit) {
|
||||||
|
branchController.moveCommitFile($branch.id, data.commit.id, commit.id, newOwnership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function acceptSquash(commit: Commit | RemoteCommit) {
|
function acceptSquash(commit: Commit | RemoteCommit) {
|
||||||
if (commit instanceof RemoteCommit) {
|
if (commit instanceof RemoteCommit) {
|
||||||
@ -104,7 +117,7 @@
|
|||||||
active: 'amend-dz-active',
|
active: 'amend-dz-active',
|
||||||
hover: 'amend-dz-hover',
|
hover: 'amend-dz-hover',
|
||||||
accepts: acceptAmend(commit),
|
accepts: acceptAmend(commit),
|
||||||
onDrop: onAmend
|
onDrop: onAmend(commit)
|
||||||
}}
|
}}
|
||||||
use:dropzone={{
|
use:dropzone={{
|
||||||
active: 'squash-dz-active',
|
active: 'squash-dz-active',
|
||||||
|
268
app/src/lib/components/CommitMessageInput.svelte
Normal file
268
app/src/lib/components/CommitMessageInput.svelte
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AIService } from '$lib/ai/service';
|
||||||
|
import { Project } from '$lib/backend/projects';
|
||||||
|
import Checkbox from '$lib/components/Checkbox.svelte';
|
||||||
|
import DropDownButton from '$lib/components/DropDownButton.svelte';
|
||||||
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
|
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||||
|
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||||
|
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||||
|
import {
|
||||||
|
projectAiGenEnabled,
|
||||||
|
projectCommitGenerationExtraConcise,
|
||||||
|
projectCommitGenerationUseEmojis
|
||||||
|
} from '$lib/config/config';
|
||||||
|
import { showError } from '$lib/notifications/toasts';
|
||||||
|
import { User } from '$lib/stores/user';
|
||||||
|
import { splitMessage } from '$lib/utils/commitMessage';
|
||||||
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
|
import { tooltip } from '$lib/utils/tooltip';
|
||||||
|
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||||
|
import { useResize } from '$lib/utils/useResize';
|
||||||
|
import { Ownership } from '$lib/vbranches/ownership';
|
||||||
|
import { Branch, LocalFile } from '$lib/vbranches/types';
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
|
export let commitMessage: string;
|
||||||
|
export let valid: boolean = false;
|
||||||
|
export let commit: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
const user = getContextStore(User);
|
||||||
|
const selectedOwnership = getContextStore(Ownership);
|
||||||
|
const aiService = getContext(AIService);
|
||||||
|
const branch = getContextStore(Branch);
|
||||||
|
const project = getContext(Project);
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
action: 'generate-branch-name';
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const aiGenEnabled = projectAiGenEnabled(project.id);
|
||||||
|
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(project.id);
|
||||||
|
const commitGenerationUseEmojis = projectCommitGenerationUseEmojis(project.id);
|
||||||
|
|
||||||
|
let aiLoading = false;
|
||||||
|
let aiConfigurationValid = false;
|
||||||
|
|
||||||
|
let contextMenu: ContextMenu;
|
||||||
|
|
||||||
|
let titleTextArea: HTMLTextAreaElement;
|
||||||
|
let descriptionTextArea: HTMLTextAreaElement;
|
||||||
|
|
||||||
|
$: ({ title, description } = splitMessage(commitMessage));
|
||||||
|
$: if (commitMessage) updateHeights();
|
||||||
|
$: valid = !!title;
|
||||||
|
|
||||||
|
function concatMessage(title: string, description: string) {
|
||||||
|
return `${title}\n\n${description}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHeights() {
|
||||||
|
useAutoHeight(titleTextArea);
|
||||||
|
useAutoHeight(descriptionTextArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusTextareaOnMount(el: HTMLTextAreaElement) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateCommitMessage(files: LocalFile[]) {
|
||||||
|
const hunks = files.flatMap((f) =>
|
||||||
|
f.hunks.filter((h) => $selectedOwnership.contains(f.id, h.id))
|
||||||
|
);
|
||||||
|
// Branches get their names generated only if there are at least 4 lines of code
|
||||||
|
// If the change is a 'one-liner', the branch name is either left as "virtual branch"
|
||||||
|
// or the user has to manually trigger the name generation from the meatball menu
|
||||||
|
// This saves people this extra click
|
||||||
|
if ($branch.name.toLowerCase().includes('virtual branch')) {
|
||||||
|
dispatch('action', 'generate-branch-name');
|
||||||
|
}
|
||||||
|
|
||||||
|
aiLoading = true;
|
||||||
|
try {
|
||||||
|
const generatedMessage = await aiService.summarizeCommit({
|
||||||
|
hunks,
|
||||||
|
useEmojiStyle: $commitGenerationUseEmojis,
|
||||||
|
useBriefStyle: $commitGenerationExtraConcise,
|
||||||
|
userToken: $user?.access_token
|
||||||
|
});
|
||||||
|
|
||||||
|
if (generatedMessage) {
|
||||||
|
commitMessage = generatedMessage;
|
||||||
|
} else {
|
||||||
|
throw new Error('Prompt generated no response');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
showError('Failed to generate commit message', e);
|
||||||
|
} finally {
|
||||||
|
aiLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
updateHeights();
|
||||||
|
descriptionTextArea.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
aiConfigurationValid = await aiService.validateConfiguration($user?.access_token);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="commit-box__textarea-wrapper text-input">
|
||||||
|
<textarea
|
||||||
|
value={title}
|
||||||
|
placeholder="Commit summary"
|
||||||
|
disabled={aiLoading}
|
||||||
|
class="text-base-body-13 text-semibold commit-box__textarea commit-box__textarea__title"
|
||||||
|
spellcheck="false"
|
||||||
|
rows="1"
|
||||||
|
bind:this={titleTextArea}
|
||||||
|
use:focusTextareaOnMount
|
||||||
|
use:useResize={() => {
|
||||||
|
useAutoHeight(titleTextArea);
|
||||||
|
}}
|
||||||
|
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||||
|
on:input={(e) => {
|
||||||
|
commitMessage = concatMessage(e.currentTarget.value, description);
|
||||||
|
}}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (commit && (e.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
|
||||||
|
if (e.key === 'Tab' || e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
descriptionTextArea.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if title.length > 0 || description}
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
disabled={aiLoading}
|
||||||
|
placeholder="Commit description (optional)"
|
||||||
|
class="text-base-body-13 commit-box__textarea commit-box__textarea__description"
|
||||||
|
spellcheck="false"
|
||||||
|
rows="1"
|
||||||
|
bind:this={descriptionTextArea}
|
||||||
|
use:useResize={() => useAutoHeight(descriptionTextArea)}
|
||||||
|
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||||
|
on:input={(e) => {
|
||||||
|
commitMessage = concatMessage(title, e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
const value = e.currentTarget.value;
|
||||||
|
if (e.key == 'Backspace' && value.length == 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
titleTextArea.focus();
|
||||||
|
useAutoHeight(e.currentTarget);
|
||||||
|
} else if (e.key == 'a' && (e.metaKey || e.ctrlKey) && value.length == 0) {
|
||||||
|
// select previous textarea on cmd+a if this textarea is empty
|
||||||
|
e.preventDefault();
|
||||||
|
titleTextArea.select();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if title.length > 50}
|
||||||
|
<div
|
||||||
|
transition:fly={{ y: 2, duration: 150 }}
|
||||||
|
class="commit-box__textarea-tooltip"
|
||||||
|
use:tooltip={{
|
||||||
|
text: '50 characters or less is best. Extra info can be added in the description.',
|
||||||
|
delay: 200
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="blitz" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="commit-box__texarea-actions"
|
||||||
|
use:tooltip={$aiGenEnabled && aiConfigurationValid
|
||||||
|
? ''
|
||||||
|
: 'You must be logged in or have provided your own API key and have summary generation enabled to use this feature'}
|
||||||
|
>
|
||||||
|
<DropDownButton
|
||||||
|
style="ghost"
|
||||||
|
kind="solid"
|
||||||
|
icon="ai-small"
|
||||||
|
disabled={!($aiGenEnabled && aiConfigurationValid)}
|
||||||
|
loading={aiLoading}
|
||||||
|
on:click={async () => await generateCommitMessage($branch.files)}
|
||||||
|
>
|
||||||
|
Generate message
|
||||||
|
<ContextMenu type="checklist" slot="context-menu" bind:this={contextMenu}>
|
||||||
|
<ContextMenuSection>
|
||||||
|
<ContextMenuItem
|
||||||
|
label="Extra concise"
|
||||||
|
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
||||||
|
>
|
||||||
|
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
|
<ContextMenuItem
|
||||||
|
label="Use emojis 😎"
|
||||||
|
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
||||||
|
>
|
||||||
|
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuSection>
|
||||||
|
</ContextMenu>
|
||||||
|
</DropDownButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.commit-box__textarea-wrapper {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 0 var(--size-48);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--size-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--size-16);
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: oklch(from var(--clr-scale-ntrl-30) l c h / 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
bottom: var(--size-12);
|
||||||
|
left: var(--size-12);
|
||||||
|
padding: var(--size-2);
|
||||||
|
border-radius: 100%;
|
||||||
|
background: var(--clr-bg-2);
|
||||||
|
color: var(--clr-scale-ntrl-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea__title {
|
||||||
|
padding: var(--size-12) var(--size-12) 0 var(--size-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__textarea__description {
|
||||||
|
padding: 0 var(--size-12) 0 var(--size-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-box__texarea-actions {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
right: var(--size-12);
|
||||||
|
bottom: var(--size-12);
|
||||||
|
}
|
||||||
|
</style>
|
@ -4,7 +4,7 @@
|
|||||||
import LargeDiffMessage from './LargeDiffMessage.svelte';
|
import LargeDiffMessage from './LargeDiffMessage.svelte';
|
||||||
import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
|
import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
|
||||||
import { tooltip } from '$lib/utils/tooltip';
|
import { tooltip } from '$lib/utils/tooltip';
|
||||||
import { getLocalCommits } from '$lib/vbranches/contexts';
|
import { getLocalCommits, getRemoteCommits } from '$lib/vbranches/contexts';
|
||||||
import { getLockText } from '$lib/vbranches/tooltip';
|
import { getLockText } from '$lib/vbranches/tooltip';
|
||||||
import type { HunkSection, ContentSection } from '$lib/utils/fileSections';
|
import type { HunkSection, ContentSection } from '$lib/utils/fileSections';
|
||||||
|
|
||||||
@ -21,6 +21,9 @@
|
|||||||
$: minWidth = getGutterMinWidth(maxLineNumber);
|
$: minWidth = getGutterMinWidth(maxLineNumber);
|
||||||
|
|
||||||
const localCommits = isFileLocked ? getLocalCommits() : undefined;
|
const localCommits = isFileLocked ? getLocalCommits() : undefined;
|
||||||
|
const remoteCommits = isFileLocked ? getRemoteCommits() : undefined;
|
||||||
|
|
||||||
|
const commits = isFileLocked ? ($localCommits || []).concat($remoteCommits || []) : undefined;
|
||||||
let alwaysShow = false;
|
let alwaysShow = false;
|
||||||
|
|
||||||
function getGutterMinWidth(max: number) {
|
function getGutterMinWidth(max: number) {
|
||||||
@ -52,10 +55,10 @@
|
|||||||
<div class="indicators text-base-11">
|
<div class="indicators text-base-11">
|
||||||
<span class="added">+{added}</span>
|
<span class="added">+{added}</span>
|
||||||
<span class="removed">-{removed}</span>
|
<span class="removed">-{removed}</span>
|
||||||
{#if section.hunk.lockedTo && $localCommits}
|
{#if section.hunk.lockedTo && section.hunk.lockedTo.length > 0 && commits}
|
||||||
<div
|
<div
|
||||||
use:tooltip={{
|
use:tooltip={{
|
||||||
text: getLockText(section.hunk.lockedTo, $localCommits),
|
text: getLockText(section.hunk.lockedTo, commits),
|
||||||
delay: 500
|
delay: 500
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
use:draggable={{
|
use:draggable={{
|
||||||
data: new DraggableFile($branch?.id || '', file, selectedFiles),
|
data: new DraggableFile($branch?.id || '', file, $commit, selectedFiles),
|
||||||
disabled: readonly || isUnapplied,
|
disabled: readonly || isUnapplied,
|
||||||
viewportId: 'board-viewport',
|
viewportId: 'board-viewport',
|
||||||
selector: '.selected-draggable'
|
selector: '.selected-draggable'
|
||||||
|
90
app/src/lib/components/History.svelte
Normal file
90
app/src/lib/components/History.svelte
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from './Button.svelte';
|
||||||
|
import { invoke } from '$lib/backend/ipc';
|
||||||
|
import { getContext } from '$lib/utils/context';
|
||||||
|
import { toHumanReadableTime } from '$lib/utils/time';
|
||||||
|
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let projectId: string;
|
||||||
|
|
||||||
|
const snapshotsLimit = 30;
|
||||||
|
|
||||||
|
const vbranchService = getContext(VirtualBranchService);
|
||||||
|
vbranchService.activeBranches.subscribe(() => {
|
||||||
|
listSnapshots(projectId, snapshotsLimit);
|
||||||
|
});
|
||||||
|
|
||||||
|
type Trailer = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
type SnapshotDetails = {
|
||||||
|
title: string;
|
||||||
|
operation: string;
|
||||||
|
body: string | undefined;
|
||||||
|
trailers: Trailer[];
|
||||||
|
};
|
||||||
|
type Snapshot = {
|
||||||
|
id: string;
|
||||||
|
details: SnapshotDetails | undefined;
|
||||||
|
createdAt: number;
|
||||||
|
};
|
||||||
|
let snapshots: Snapshot[] = [];
|
||||||
|
async function listSnapshots(projectId: string, limit: number) {
|
||||||
|
const resp = await invoke<Snapshot[]>('list_snapshots', {
|
||||||
|
projectId: projectId,
|
||||||
|
limit: limit
|
||||||
|
});
|
||||||
|
console.log(resp);
|
||||||
|
snapshots = resp;
|
||||||
|
}
|
||||||
|
async function restoreSnapshot(projectId: string, sha: string) {
|
||||||
|
const resp = await invoke<string>('restore_snapshot', {
|
||||||
|
projectId: projectId,
|
||||||
|
sha: sha
|
||||||
|
});
|
||||||
|
console.log(resp);
|
||||||
|
}
|
||||||
|
onMount(async () => {
|
||||||
|
listSnapshots(projectId, snapshotsLimit);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{#each snapshots as entry, idx}
|
||||||
|
<div class="card">
|
||||||
|
<div class="entry">
|
||||||
|
<div>
|
||||||
|
{entry.details?.operation}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{toHumanReadableTime(entry.createdAt)}
|
||||||
|
</span>
|
||||||
|
{#if idx != 0}
|
||||||
|
<Button
|
||||||
|
style="pop"
|
||||||
|
size="tag"
|
||||||
|
icon="undo-small"
|
||||||
|
on:click={async () => await restoreSnapshot(projectId, entry.id)}>restore</Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 50rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-left-width: 1px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.entry {
|
||||||
|
flex: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
@ -158,19 +158,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
background-color: var(--clr-theme-err-container);
|
background-color: var(--clr-theme-err-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pop {
|
&.pop {
|
||||||
background-color: var(--clr-theme-pop-container);
|
background-color: var(--clr-theme-pop-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
background-color: var(--clr-theme-warn-container);
|
background-color: var(--clr-theme-warn-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
background-color: var(--clr-theme-succ-container);
|
background-color: var(--clr-theme-succ-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,44 +1,55 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Overlay from './Overlay.svelte';
|
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import OutClick from 'svelte-outclick';
|
||||||
import type iconsJson from '$lib/icons/icons.json';
|
import type iconsJson from '$lib/icons/icons.json';
|
||||||
|
|
||||||
export function show(newItem?: any) {
|
let dialog: HTMLDialogElement;
|
||||||
item = newItem;
|
let item: any;
|
||||||
modal.show();
|
let open = false;
|
||||||
}
|
|
||||||
export function close() {
|
|
||||||
item = undefined;
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
export let width: 'default' | 'small' | 'large' = 'default';
|
export let width: 'default' | 'small' | 'large' = 'default';
|
||||||
export let title: string | undefined = undefined;
|
export let title: string | undefined = undefined;
|
||||||
export let icon: keyof typeof iconsJson | undefined = undefined;
|
export let icon: keyof typeof iconsJson | undefined = undefined;
|
||||||
export let hoverText: string | undefined = undefined;
|
|
||||||
|
|
||||||
let item: any;
|
export function show(newItem?: any) {
|
||||||
let modal: Overlay;
|
item = newItem;
|
||||||
|
dialog.showModal();
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function close() {
|
||||||
|
item = undefined;
|
||||||
|
dialog.close();
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.body.appendChild(dialog);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Overlay bind:this={modal} let:close on:close {width}>
|
<dialog
|
||||||
<form on:submit>
|
class="dialog-wrap"
|
||||||
|
class:s-default={width == 'default'}
|
||||||
|
class:s-small={width == 'small'}
|
||||||
|
class:s-large={width == 'large'}
|
||||||
|
bind:this={dialog}
|
||||||
|
on:close={close}
|
||||||
|
>
|
||||||
|
{#if open}
|
||||||
|
<OutClick on:outclick={close}>
|
||||||
|
<div class="dialog">
|
||||||
|
<form class="modal-content" on:submit>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="modal__header">
|
<div class="modal__header">
|
||||||
<div class="modal__header__content" class:adjust-header={$$slots.header_controls}>
|
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
{/if}
|
{/if}
|
||||||
<h2 class="text-base-14 text-semibold" title={hoverText}>
|
<h2 class="text-base-14 text-semibold">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
{#if $$slots.header_controls}
|
|
||||||
<div class="modal__header__actions">
|
|
||||||
<slot name="header_controls" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="modal__body custom-scrollbar">
|
<div class="modal__body custom-scrollbar">
|
||||||
@ -51,9 +62,41 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
</Overlay>
|
</div>
|
||||||
|
</OutClick>
|
||||||
|
{/if}
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
.dialog-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(100vh - 5rem);
|
||||||
|
border-radius: var(--radius-l);
|
||||||
|
background-color: var(--clr-bg-1);
|
||||||
|
border: 1px solid var(--clr-border-2);
|
||||||
|
box-shadow: var(--fx-shadow-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* modifiers */
|
||||||
|
|
||||||
|
.s-large {
|
||||||
|
max-width: calc(var(--size-64) * 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-default {
|
||||||
|
max-width: calc(var(--size-64) * 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.s-small {
|
||||||
|
max-width: calc(var(--size-64) * 6);
|
||||||
|
}
|
||||||
|
|
||||||
.modal__header {
|
.modal__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: var(--size-16);
|
padding: var(--size-16);
|
||||||
@ -61,17 +104,6 @@
|
|||||||
border-bottom: 1px solid var(--clr-border-2);
|
border-bottom: 1px solid var(--clr-border-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__header__content {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--size-8);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__header__actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--size-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__body {
|
.modal__body {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: var(--size-16);
|
padding: var(--size-16);
|
||||||
@ -86,8 +118,4 @@
|
|||||||
border-top: 1px solid var(--clr-border-2);
|
border-top: 1px solid var(--clr-border-2);
|
||||||
background-color: var(--clr-bg-1);
|
background-color: var(--clr-bg-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.adjust-header {
|
|
||||||
margin-top: var(--size-6);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import OutClick from 'svelte-outclick';
|
|
||||||
|
|
||||||
let dialog: HTMLDialogElement;
|
|
||||||
|
|
||||||
let open = false;
|
|
||||||
|
|
||||||
export let width: 'default' | 'small' | 'large' = 'default';
|
|
||||||
|
|
||||||
export function show() {
|
|
||||||
if (open) return;
|
|
||||||
dialog.showModal();
|
|
||||||
open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function close() {
|
|
||||||
if (!open) return;
|
|
||||||
dialog.close();
|
|
||||||
open = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<dialog
|
|
||||||
class="dialog"
|
|
||||||
class:open-modal={open}
|
|
||||||
class:s-default={width == 'default'}
|
|
||||||
class:s-small={width == 'small'}
|
|
||||||
class:s-large={width == 'large'}
|
|
||||||
bind:this={dialog}
|
|
||||||
on:close={close}
|
|
||||||
on:close
|
|
||||||
>
|
|
||||||
{#if open}
|
|
||||||
<OutClick on:outclick={close}>
|
|
||||||
<slot {close} isOpen={open} />
|
|
||||||
</OutClick>
|
|
||||||
{/if}
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.dialog {
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
max-height: calc(100vh - 5rem);
|
|
||||||
border-radius: var(--radius-l);
|
|
||||||
background-color: var(--clr-bg-1);
|
|
||||||
border: 1px solid var(--clr-border-2);
|
|
||||||
box-shadow: var(--fx-shadow-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* modifiers */
|
|
||||||
|
|
||||||
.s-large {
|
|
||||||
max-width: calc(var(--size-64) * 13);
|
|
||||||
}
|
|
||||||
|
|
||||||
.s-default {
|
|
||||||
max-width: calc(var(--size-64) * 9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.s-small {
|
|
||||||
max-width: calc(var(--size-64) * 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.open-modal {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -87,7 +87,7 @@
|
|||||||
color: var(--clr-scale-ntrl-0);
|
color: var(--clr-scale-ntrl-0);
|
||||||
gap: var(--size-12);
|
gap: var(--size-12);
|
||||||
padding: var(--size-20);
|
padding: var(--size-20);
|
||||||
background-color: var(--clr-theme-err-container);
|
background-color: var(--clr-theme-err-bg);
|
||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
margin-bottom: var(--size-12);
|
margin-bottom: var(--size-12);
|
||||||
}
|
}
|
||||||
|
@ -122,11 +122,11 @@
|
|||||||
function getChecksCount(status: ChecksStatus): string {
|
function getChecksCount(status: ChecksStatus): string {
|
||||||
if (!status) return 'Running checks';
|
if (!status) return 'Running checks';
|
||||||
|
|
||||||
|
const completed = status.completed || 0;
|
||||||
const skipped = status.skipped || 0;
|
const skipped = status.skipped || 0;
|
||||||
const total = (status.totalCount || 0) - skipped;
|
const total = (status.totalCount || 0) - skipped;
|
||||||
const queued = total - (status.queued || 0);
|
|
||||||
|
|
||||||
return `Running checks ${queued}/${total}`;
|
return `Checks completed ${completed}/${total}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChecksTagInfo(
|
function getChecksTagInfo(
|
||||||
|
@ -48,7 +48,9 @@
|
|||||||
reversedDirection
|
reversedDirection
|
||||||
loading={isDeleting}
|
loading={isDeleting}
|
||||||
icon="bin-small"
|
icon="bin-small"
|
||||||
on:click={onDeleteClicked}>Remove</Button
|
on:click={() => {
|
||||||
|
onDeleteClicked().then(close);
|
||||||
|
}}>Remove</Button
|
||||||
>
|
>
|
||||||
<Button style="pop" kind="solid" on:click={close}>Cancel</Button>
|
<Button style="pop" kind="solid" on:click={close}>Cancel</Button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -93,11 +93,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
background: var(--clr-theme-pop-container);
|
background: var(--clr-theme-pop-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background: var(--clr-theme-warn-container);
|
background: var(--clr-theme-warn-bg);
|
||||||
}
|
}
|
||||||
.extra-padding {
|
.extra-padding {
|
||||||
padding: var(--size-20);
|
padding: var(--size-20);
|
||||||
|
@ -34,12 +34,6 @@
|
|||||||
dispatch('select', { value });
|
dispatch('select', { value });
|
||||||
listOpen = false;
|
listOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollIntoView() {
|
|
||||||
const selected = element.querySelector('.selected');
|
|
||||||
if (selected) selected.scrollIntoView();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMaxHeight() {
|
function setMaxHeight() {
|
||||||
maxHeight = window.innerHeight - element.getBoundingClientRect().bottom - maxPadding;
|
maxHeight = window.innerHeight - element.getBoundingClientRect().bottom - maxPadding;
|
||||||
}
|
}
|
||||||
@ -52,7 +46,6 @@
|
|||||||
function openList() {
|
function openList() {
|
||||||
setMaxHeight();
|
setMaxHeight();
|
||||||
listOpen = true;
|
listOpen = true;
|
||||||
setTimeout(() => scrollIntoView(), 50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeList() {
|
function closeList() {
|
||||||
|
@ -59,4 +59,12 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: var(--clr-bg-2);
|
||||||
|
|
||||||
|
& .label {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.success.setup-feature {
|
.success.setup-feature {
|
||||||
background: var(--clr-theme-pop-container, #f3fcfb);
|
background: var(--clr-theme-pop-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-feature__content {
|
.setup-feature__content {
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
color: var(--clr-scale-ntrl-0);
|
color: var(--clr-scale-ntrl-0);
|
||||||
gap: var(--size-12);
|
gap: var(--size-12);
|
||||||
padding: var(--size-20);
|
padding: var(--size-20);
|
||||||
background-color: var(--clr-theme-err-container);
|
background-color: var(--clr-theme-err-bg);
|
||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
margin-bottom: var(--size-12);
|
margin-bottom: var(--size-12);
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@
|
|||||||
|
|
||||||
.pop {
|
.pop {
|
||||||
&.soft {
|
&.soft {
|
||||||
color: var(--clr-theme-pop-on-container);
|
color: var(--clr-theme-pop-on-soft);
|
||||||
background: var(--clr-scale-pop-80);
|
background: var(--clr-scale-pop-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-button, &:disabled):hover {
|
&:not(.not-button, &:disabled):hover {
|
||||||
@ -134,7 +134,7 @@
|
|||||||
|
|
||||||
.success {
|
.success {
|
||||||
&.soft {
|
&.soft {
|
||||||
color: var(--clr-theme-succ-on-container);
|
color: var(--clr-theme-succ-on-soft);
|
||||||
background: var(--clr-scale-succ-80);
|
background: var(--clr-scale-succ-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-button, &:disabled):hover {
|
&:not(.not-button, &:disabled):hover {
|
||||||
@ -154,7 +154,7 @@
|
|||||||
|
|
||||||
.error {
|
.error {
|
||||||
&.soft {
|
&.soft {
|
||||||
color: var(--clr-theme-err-on-container);
|
color: var(--clr-theme-err-on-soft);
|
||||||
background: var(--clr-scale-err-80);
|
background: var(--clr-scale-err-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-button, &:disabled):hover {
|
&:not(.not-button, &:disabled):hover {
|
||||||
@ -174,7 +174,7 @@
|
|||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
&.soft {
|
&.soft {
|
||||||
color: var(--clr-theme-warn-on-container);
|
color: var(--clr-theme-warn-on-soft);
|
||||||
background: var(--clr-scale-warn-80);
|
background: var(--clr-scale-warn-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-button, &:disabled):hover {
|
&:not(.not-button, &:disabled):hover {
|
||||||
@ -194,7 +194,7 @@
|
|||||||
|
|
||||||
.purple {
|
.purple {
|
||||||
&.soft {
|
&.soft {
|
||||||
color: var(--clr-theme-purp-on-container);
|
color: var(--clr-theme-purp-on-soft);
|
||||||
background: var(--clr-scale-purp-80);
|
background: var(--clr-scale-purp-80);
|
||||||
/* if button */
|
/* if button */
|
||||||
&:not(.not-button, &:disabled):hover {
|
&:not(.not-button, &:disabled):hover {
|
||||||
@ -214,43 +214,15 @@
|
|||||||
|
|
||||||
/* modifiers */
|
/* modifiers */
|
||||||
|
|
||||||
.not-button {
|
|
||||||
cursor: default;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/* opacity: 0.5; */
|
opacity: 0.6;
|
||||||
|
|
||||||
&.neutral.solid,
|
|
||||||
&.pop.solid,
|
|
||||||
&.success.solid,
|
|
||||||
&.error.solid,
|
|
||||||
&.warning.solid,
|
|
||||||
&.purple.solid {
|
|
||||||
color: var(--clr-text-2);
|
|
||||||
background: oklch(from var(--clr-scale-ntrl-60) l c h / 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.neutral.soft,
|
.not-button {
|
||||||
&.pop.soft,
|
cursor: default;
|
||||||
&.success.soft,
|
user-select: none;
|
||||||
&.error.soft,
|
|
||||||
&.warning.soft,
|
|
||||||
&.purple.soft {
|
|
||||||
color: var(--clr-text-2);
|
|
||||||
background: oklch(from var(--clr-scale-ntrl-60) l c h / 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ghost {
|
|
||||||
color: var(--clr-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ghost.solid {
|
|
||||||
border: 1px solid oklch(from var(--clr-scale-ntrl-0) l c h / 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reversedDirection {
|
.reversedDirection {
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<div class="textbox__icon">
|
<div class="textbox__icon">
|
||||||
<Icon name={icon} />
|
<Icon name={!disabled ? icon : 'locked'} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -152,9 +152,19 @@
|
|||||||
|
|
||||||
.textbox__input-wrap {
|
.textbox__input-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
&.disabled .textbox__icon {
|
|
||||||
|
&.disabled {
|
||||||
|
/* background-color: var(--clr-bg-1); */
|
||||||
|
& .textbox__icon {
|
||||||
color: var(--clr-scale-ntrl-60);
|
color: var(--clr-scale-ntrl-60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .textbox__input {
|
||||||
|
color: var(--clr-text-2);
|
||||||
|
background-color: var(--clr-bg-2);
|
||||||
|
border: 1px solid var(--clr-border-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textbox__input {
|
.textbox__input {
|
||||||
@ -198,7 +208,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* select */
|
/* select */
|
||||||
.textbox__input[type='select'] {
|
.textbox__input[type='select']:not([disabled]),
|
||||||
|
.textbox__input[type='select']:not([readonly]) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +101,16 @@
|
|||||||
<span class="text-base-14 text-semibold">Telemetry</span>
|
<span class="text-base-14 text-semibold">Telemetry</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="profile-sidebar__menu-item"
|
||||||
|
class:item_selected={currentSection == 'experimental'}
|
||||||
|
on:mousedown={() => onMenuClick('experimental')}
|
||||||
|
>
|
||||||
|
<Icon name="idea" />
|
||||||
|
<span class="text-base-14 text-semibold">Experimental</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -35,6 +35,15 @@ export function appErrorReportingEnabled() {
|
|||||||
return persisted(true, 'appErrorReportingEnabled');
|
return persisted(true, 'appErrorReportingEnabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a writable store for obtaining or setting the current state of non-anonemous application metrics.
|
||||||
|
* The setting can be enabled or disabled by setting the value of the store to true or false.
|
||||||
|
* @returns A writable store with the appNonAnonMetricsEnabled config.
|
||||||
|
*/
|
||||||
|
export function appNonAnonMetricsEnabled() {
|
||||||
|
return persisted(false, 'appNonAnonMetricsEnabled');
|
||||||
|
}
|
||||||
|
|
||||||
function persisted<T>(initial: T, key: string): Writable<T> & { onDisk: () => Promise<T> } {
|
function persisted<T>(initial: T, key: string): Writable<T> & { onDisk: () => Promise<T> } {
|
||||||
async function setAndPersist(value: T, set: (value: T) => void) {
|
async function setAndPersist(value: T, set: (value: T) => void) {
|
||||||
await store.set(key, value);
|
await store.set(key, value);
|
||||||
|
17
app/src/lib/config/uiFeatureFlags.ts
Normal file
17
app/src/lib/config/uiFeatureFlags.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* This file contains functions for managing ui-specific feature flags.
|
||||||
|
* The values are persisted in local storage. Entries are prefixed with 'feature'.
|
||||||
|
*
|
||||||
|
* @module appSettings
|
||||||
|
*/
|
||||||
|
import { persisted, type Persisted } from '$lib/persisted/persisted';
|
||||||
|
|
||||||
|
export function featureBaseBranchSwitching(): Persisted<boolean> {
|
||||||
|
const key = 'featureBaseBranchSwitching';
|
||||||
|
return persisted(false, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function featureAdvancedCommitOperations(): Persisted<boolean> {
|
||||||
|
const key = 'featureAdvancedCommitOperations';
|
||||||
|
return persisted(false, key);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import type { AnyFile, Commit, Hunk, RemoteCommit } from '../vbranches/types';
|
import type { AnyCommit, AnyFile, Commit, Hunk, RemoteCommit } from '../vbranches/types';
|
||||||
|
|
||||||
export function nonDraggable() {
|
export function nonDraggable() {
|
||||||
return {
|
return {
|
||||||
@ -18,7 +18,8 @@ export class DraggableHunk {
|
|||||||
export class DraggableFile {
|
export class DraggableFile {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly branchId: string,
|
public readonly branchId: string,
|
||||||
private file: AnyFile,
|
public file: AnyFile,
|
||||||
|
public commit: AnyCommit | undefined,
|
||||||
private selection: Readable<AnyFile[]> | undefined
|
private selection: Readable<AnyFile[]> | undefined
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -22,7 +22,8 @@ export function showToast(toast: Toast) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function showError(title: string, err: any) {
|
export function showError(title: string, err: any) {
|
||||||
if (err.message) showToast({ title, errorMessage: err.message, style: 'error' });
|
const errorMessage = err.message ? err.message : err;
|
||||||
|
showToast({ title, errorMessage: errorMessage, style: 'error' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dismissToast(messageId: string | undefined) {
|
export function dismissToast(messageId: string | undefined) {
|
||||||
|
@ -17,6 +17,7 @@ export interface Settings {
|
|||||||
zoom: number;
|
zoom: number;
|
||||||
scrollbarVisabilityOnHover: boolean;
|
scrollbarVisabilityOnHover: boolean;
|
||||||
tabSize: number;
|
tabSize: number;
|
||||||
|
showHistoryView: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults: Settings = {
|
const defaults: Settings = {
|
||||||
@ -31,7 +32,8 @@ const defaults: Settings = {
|
|||||||
stashedBranchesHeight: 150,
|
stashedBranchesHeight: 150,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
scrollbarVisabilityOnHover: false,
|
scrollbarVisabilityOnHover: false,
|
||||||
tabSize: 4
|
tabSize: 4,
|
||||||
|
showHistoryView: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadUserSettings(): Writable<Settings> {
|
export function loadUserSettings(): Writable<Settings> {
|
||||||
|
16
app/src/lib/utils/branch.test.ts
Normal file
16
app/src/lib/utils/branch.test.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { normalizeBranchName } from '$lib/utils/branch';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
describe.concurrent('normalizeBranchName', () => {
|
||||||
|
test('it should remove undesirable symbols', () => {
|
||||||
|
expect(normalizeBranchName('a£^&*() b')).toEqual('a-b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should preserve capital letters', () => {
|
||||||
|
expect(normalizeBranchName('Hello World')).toEqual('Hello-World');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it should preserve `#`, `_`, `/`, and `.`', () => {
|
||||||
|
expect(normalizeBranchName('hello#_./world')).toEqual('hello#_./world');
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,3 @@
|
|||||||
export function normalizeBranchName(value: string) {
|
export function normalizeBranchName(value: string) {
|
||||||
return value.toLowerCase().replace(/[^0-9a-z/_.]+/g, '-');
|
return value.replace(/[^A-Za-z0-9_/.#]+/g, '-');
|
||||||
}
|
}
|
||||||
|
4
app/src/lib/utils/filters.ts
Normal file
4
app/src/lib/utils/filters.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// If a value occurs > 1 times then all but one will fail this condition.
|
||||||
|
export function unique(value: any, index: number, array: any[]) {
|
||||||
|
return array.indexOf(value) === index;
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
export function isDefined<T>(file: T | undefined): file is T {
|
export function isDefined<T>(file: T | undefined | null): file is T {
|
||||||
return file !== undefined;
|
return file !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function notNull<T>(file: T | undefined | null): file is T {
|
||||||
|
return file !== null;
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ export class BranchController {
|
|||||||
async setTarget(branch: string) {
|
async setTarget(branch: string) {
|
||||||
try {
|
try {
|
||||||
await this.targetBranchService.setTarget(branch);
|
await this.targetBranchService.setTarget(branch);
|
||||||
|
return branch;
|
||||||
// TODO: Reloading seems to trigger 4 invocations of `list_virtual_branches`
|
// TODO: Reloading seems to trigger 4 invocations of `list_virtual_branches`
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showError('Failed to set base branch', err);
|
showError('Failed to set base branch', err);
|
||||||
@ -293,11 +294,12 @@ export class BranchController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async amendBranch(branchId: string, ownership: string) {
|
async amendBranch(branchId: string, commitOid: string, ownership: string) {
|
||||||
try {
|
try {
|
||||||
await invoke<void>('amend_virtual_branch', {
|
await invoke<void>('amend_virtual_branch', {
|
||||||
projectId: this.projectId,
|
projectId: this.projectId,
|
||||||
branchId,
|
branchId,
|
||||||
|
commitOid,
|
||||||
ownership
|
ownership
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -305,6 +307,76 @@ export class BranchController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async moveCommitFile(
|
||||||
|
branchId: string,
|
||||||
|
fromCommitOid: string,
|
||||||
|
toCommitOid: string,
|
||||||
|
ownership: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await invoke<void>('move_commit_file', {
|
||||||
|
projectId: this.projectId,
|
||||||
|
branchId,
|
||||||
|
fromCommitOid,
|
||||||
|
toCommitOid,
|
||||||
|
ownership
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
showError('Failed to amend commit', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async undoCommit(branchId: string, commitOid: string) {
|
||||||
|
try {
|
||||||
|
await invoke<void>('undo_commit', {
|
||||||
|
projectId: this.projectId,
|
||||||
|
branchId,
|
||||||
|
commitOid
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
showError('Failed to amend commit', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCommitMessage(branchId: string, commitOid: string, message: string) {
|
||||||
|
try {
|
||||||
|
await invoke<void>('update_commit_message', {
|
||||||
|
projectId: this.projectId,
|
||||||
|
branchId,
|
||||||
|
commitOid,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
showError('Failed to change commit message', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertBlankCommit(branchId: string, commitOid: string, offset: number) {
|
||||||
|
try {
|
||||||
|
await invoke<void>('insert_blank_commit', {
|
||||||
|
projectId: this.projectId,
|
||||||
|
branchId,
|
||||||
|
commitOid,
|
||||||
|
offset
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
showError('Failed to insert blank commit', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reorderCommit(branchId: string, commitOid: string, offset: number) {
|
||||||
|
try {
|
||||||
|
await invoke<void>('reorder_commit', {
|
||||||
|
projectId: this.projectId,
|
||||||
|
branchId,
|
||||||
|
commitOid,
|
||||||
|
offset
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
showError('Failed to reorder blank commit', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async moveCommit(targetBranchId: string, commitOid: string) {
|
async moveCommit(targetBranchId: string, commitOid: string) {
|
||||||
try {
|
try {
|
||||||
await invoke<void>('move_commit', {
|
await invoke<void>('move_commit', {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Branch, AnyFile, Hunk, RemoteHunk } from './types';
|
import type { Branch, AnyFile, Hunk, RemoteHunk, RemoteFile } from './types';
|
||||||
|
|
||||||
export function filesToOwnership(files: AnyFile[]) {
|
export function filesToOwnership(files: AnyFile[]) {
|
||||||
return files
|
return files
|
||||||
@ -6,6 +6,15 @@ export function filesToOwnership(files: AnyFile[]) {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filesToSimpleOwnership(files: RemoteFile[]) {
|
||||||
|
return files
|
||||||
|
.map(
|
||||||
|
(f) =>
|
||||||
|
`${f.path}:${f.hunks.map(({ new_start, new_lines }) => `${new_start}-${new_start + new_lines}`).join(',')}`
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// These types help keep track of what maps to what.
|
// These types help keep track of what maps to what.
|
||||||
// TODO: refactor code for clarity, these types should not be needed
|
// TODO: refactor code for clarity, these types should not be needed
|
||||||
export type AnyHunk = Hunk | RemoteHunk;
|
export type AnyHunk = Hunk | RemoteHunk;
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import type { Commit } from './types';
|
import { HunkLock, type Commit } from './types';
|
||||||
|
import { unique } from '$lib/utils/filters';
|
||||||
|
|
||||||
export function getLockText(commitId: string[] | string, commits: Commit[]): string {
|
export function getLockText(hunkLocks: HunkLock | HunkLock[] | string, commits: Commit[]): string {
|
||||||
if (!commitId || commits === undefined) return 'Depends on a committed change';
|
if (!hunkLocks || commits === undefined) return 'Depends on a committed change';
|
||||||
|
|
||||||
const lockedIds = typeof commitId == 'string' ? [commitId] : (commitId as string[]);
|
const locks = hunkLocks instanceof HunkLock ? [hunkLocks] : (hunkLocks as HunkLock[]);
|
||||||
|
|
||||||
const descriptions = lockedIds
|
const descriptions = locks
|
||||||
.map((id) => {
|
.filter(unique)
|
||||||
const commit = commits.find((commit) => commit.id == id);
|
.map((lock) => {
|
||||||
|
const commit = commits.find((c) => {
|
||||||
|
return c.id == lock.commitId;
|
||||||
|
});
|
||||||
const shortCommitId = commit?.id.slice(0, 7);
|
const shortCommitId = commit?.id.slice(0, 7);
|
||||||
if (commit) {
|
if (commit) {
|
||||||
const shortTitle = commit.descriptionTitle?.slice(0, 35) + '...';
|
const shortTitle = commit.descriptionTitle?.slice(0, 35) + '...';
|
||||||
@ -17,5 +21,13 @@ export function getLockText(commitId: string[] | string, commits: Commit[]): str
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
return 'Locked due to dependency on:\n' + descriptions;
|
const branchCount = locks.map((lock) => lock.branchId).filter(unique).length;
|
||||||
|
if (branchCount > 1) {
|
||||||
|
return (
|
||||||
|
'Warning, undefined behavior due to lock on multiple branches!\n\n' +
|
||||||
|
'Locked because changes depend on:\n' +
|
||||||
|
descriptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 'Locked because changes depend on:\n' + descriptions;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import { splitMessage } from '$lib/utils/commitMessage';
|
import { splitMessage } from '$lib/utils/commitMessage';
|
||||||
import { hashCode } from '$lib/utils/string';
|
import { hashCode } from '$lib/utils/string';
|
||||||
|
import { isDefined, notNull } from '$lib/utils/typeguards';
|
||||||
import { Type, Transform } from 'class-transformer';
|
import { Type, Transform } from 'class-transformer';
|
||||||
|
|
||||||
export type ChangeType =
|
export type ChangeType =
|
||||||
@ -21,8 +22,16 @@ export class Hunk {
|
|||||||
filePath!: string;
|
filePath!: string;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
locked!: boolean;
|
locked!: boolean;
|
||||||
lockedTo!: string | undefined;
|
@Type(() => HunkLock)
|
||||||
|
lockedTo!: HunkLock[];
|
||||||
changeType!: ChangeType;
|
changeType!: ChangeType;
|
||||||
|
new_start!: number;
|
||||||
|
new_lines!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HunkLock {
|
||||||
|
branchId!: string;
|
||||||
|
commitId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyFile = LocalFile | RemoteFile;
|
export type AnyFile = LocalFile | RemoteFile;
|
||||||
@ -58,14 +67,15 @@ export class LocalFile {
|
|||||||
|
|
||||||
get locked(): boolean {
|
get locked(): boolean {
|
||||||
return this.hunks
|
return this.hunks
|
||||||
? this.hunks.map((hunk) => hunk.lockedTo).reduce((a, b) => !!(a || b), false)
|
? this.hunks.map((hunk) => hunk.locked).reduce((a, b) => !!(a || b), false)
|
||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get lockedIds(): string[] {
|
get lockedIds(): HunkLock[] {
|
||||||
return this.hunks
|
return this.hunks
|
||||||
.map((hunk) => hunk.lockedTo)
|
.flatMap((hunk) => hunk.lockedTo)
|
||||||
.filter((lockedTo): lockedTo is string => !!lockedTo);
|
.filter(notNull)
|
||||||
|
.filter(isDefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,6 +220,8 @@ export const UNKNOWN_COMMITS = Symbol('UnknownCommits');
|
|||||||
export class RemoteHunk {
|
export class RemoteHunk {
|
||||||
diff!: string;
|
diff!: string;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
|
new_start!: number;
|
||||||
|
new_lines!: number;
|
||||||
|
|
||||||
get id(): string {
|
get id(): string {
|
||||||
return hashCode(this.diff);
|
return hashCode(this.diff);
|
||||||
@ -250,7 +262,7 @@ export class RemoteFile {
|
|||||||
return this.hunks.map((h) => h.id);
|
return this.hunks.map((h) => h.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get lockedIds(): string[] {
|
get lockedIds(): HunkLock[] {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +67,12 @@
|
|||||||
hotkeys.on('Backspace', (e) => {
|
hotkeys.on('Backspace', (e) => {
|
||||||
// This prevent backspace from navigating back
|
// This prevent backspace from navigating back
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
}),
|
||||||
|
hotkeys.on('$mod+Shift+H', () => {
|
||||||
|
userSettings.update((s) => ({
|
||||||
|
...s,
|
||||||
|
showHistoryView: !$userSettings.showHistoryView
|
||||||
|
}));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { AIService } from '$lib/ai/service';
|
import { AIService } from '$lib/ai/service';
|
||||||
import { initPostHog } from '$lib/analytics/posthog';
|
import { initAnalyticsIfEnabled } from '$lib/analytics/analytics';
|
||||||
import { initSentry } from '$lib/analytics/sentry';
|
|
||||||
import { AuthService } from '$lib/backend/auth';
|
import { AuthService } from '$lib/backend/auth';
|
||||||
import { GitConfigService } from '$lib/backend/gitConfigService';
|
import { GitConfigService } from '$lib/backend/gitConfigService';
|
||||||
import { HttpClient } from '$lib/backend/httpClient';
|
import { HttpClient } from '$lib/backend/httpClient';
|
||||||
import { ProjectService } from '$lib/backend/projects';
|
import { ProjectService } from '$lib/backend/projects';
|
||||||
import { PromptService } from '$lib/backend/prompt';
|
import { PromptService } from '$lib/backend/prompt';
|
||||||
import { UpdaterService } from '$lib/backend/updater';
|
import { UpdaterService } from '$lib/backend/updater';
|
||||||
import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings';
|
|
||||||
import { GitHubService } from '$lib/github/service';
|
import { GitHubService } from '$lib/github/service';
|
||||||
import { UserService } from '$lib/stores/user';
|
import { UserService } from '$lib/stores/user';
|
||||||
import lscache from 'lscache';
|
import lscache from 'lscache';
|
||||||
@ -24,16 +22,7 @@ export const prerender = false;
|
|||||||
export const csr = true;
|
export const csr = true;
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
appErrorReportingEnabled()
|
initAnalyticsIfEnabled();
|
||||||
.onDisk()
|
|
||||||
.then((enabled) => {
|
|
||||||
if (enabled) initSentry();
|
|
||||||
});
|
|
||||||
appMetricsEnabled()
|
|
||||||
.onDisk()
|
|
||||||
.then((enabled) => {
|
|
||||||
if (enabled) initPostHog();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Find a workaround to avoid this dynamic import
|
// TODO: Find a workaround to avoid this dynamic import
|
||||||
// https://github.com/sveltejs/kit/issues/905
|
// https://github.com/sveltejs/kit/issues/905
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
import { Project } from '$lib/backend/projects';
|
import { Project } from '$lib/backend/projects';
|
||||||
import { syncToCloud } from '$lib/backend/sync';
|
import { syncToCloud } from '$lib/backend/sync';
|
||||||
import { BranchService } from '$lib/branches/service';
|
import { BranchService } from '$lib/branches/service';
|
||||||
|
import History from '$lib/components/History.svelte';
|
||||||
import Navigation from '$lib/components/Navigation.svelte';
|
import Navigation from '$lib/components/Navigation.svelte';
|
||||||
import NoBaseBranch from '$lib/components/NoBaseBranch.svelte';
|
import NoBaseBranch from '$lib/components/NoBaseBranch.svelte';
|
||||||
import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte';
|
import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte';
|
||||||
import ProblemLoadingRepo from '$lib/components/ProblemLoadingRepo.svelte';
|
import ProblemLoadingRepo from '$lib/components/ProblemLoadingRepo.svelte';
|
||||||
import ProjectSettingsMenuAction from '$lib/components/ProjectSettingsMenuAction.svelte';
|
import ProjectSettingsMenuAction from '$lib/components/ProjectSettingsMenuAction.svelte';
|
||||||
|
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
||||||
|
import { getContextStoreBySymbol } from '$lib/utils/context';
|
||||||
import * as hotkeys from '$lib/utils/hotkeys';
|
import * as hotkeys from '$lib/utils/hotkeys';
|
||||||
import { unsubscribe } from '$lib/utils/unsubscribe';
|
import { unsubscribe } from '$lib/utils/unsubscribe';
|
||||||
import { BaseBranchService, NoDefaultTarget } from '$lib/vbranches/baseBranch';
|
import { BaseBranchService, NoDefaultTarget } from '$lib/vbranches/baseBranch';
|
||||||
@ -33,6 +36,7 @@
|
|||||||
$: baseBranch = baseBranchService.base;
|
$: baseBranch = baseBranchService.base;
|
||||||
$: baseError = baseBranchService.error;
|
$: baseError = baseBranchService.error;
|
||||||
$: projectError = projectService.error;
|
$: projectError = projectService.error;
|
||||||
|
const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
|
||||||
|
|
||||||
$: setContext(VirtualBranchService, vbranchService);
|
$: setContext(VirtualBranchService, vbranchService);
|
||||||
$: setContext(BranchController, branchController);
|
$: setContext(BranchController, branchController);
|
||||||
@ -90,6 +94,9 @@
|
|||||||
<div class="view-wrap" role="group" on:dragover|preventDefault>
|
<div class="view-wrap" role="group" on:dragover|preventDefault>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<slot />
|
<slot />
|
||||||
|
{#if $userSettings.showHistoryView}
|
||||||
|
<History {projectId} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Project, ProjectService } from '$lib/backend/projects';
|
import { Project, ProjectService } from '$lib/backend/projects';
|
||||||
|
import BaseBranchSwitch from '$lib/components/BaseBranchSwitch.svelte';
|
||||||
import CloudForm from '$lib/components/CloudForm.svelte';
|
import CloudForm from '$lib/components/CloudForm.svelte';
|
||||||
import DetailsForm from '$lib/components/DetailsForm.svelte';
|
import DetailsForm from '$lib/components/DetailsForm.svelte';
|
||||||
import KeysForm from '$lib/components/KeysForm.svelte';
|
import KeysForm from '$lib/components/KeysForm.svelte';
|
||||||
@ -8,6 +9,7 @@
|
|||||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||||
import Spacer from '$lib/components/Spacer.svelte';
|
import Spacer from '$lib/components/Spacer.svelte';
|
||||||
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
|
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
|
||||||
|
import { featureBaseBranchSwitching } from '$lib/config/uiFeatureFlags';
|
||||||
import { showError } from '$lib/notifications/toasts';
|
import { showError } from '$lib/notifications/toasts';
|
||||||
import { getContext } from '$lib/utils/context';
|
import { getContext } from '$lib/utils/context';
|
||||||
import * as toasts from '$lib/utils/toasts';
|
import * as toasts from '$lib/utils/toasts';
|
||||||
@ -15,6 +17,7 @@
|
|||||||
import { from } from 'rxjs';
|
import { from } from 'rxjs';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
const baseBranchSwitching = featureBaseBranchSwitching();
|
||||||
const projectService = getContext(ProjectService);
|
const projectService = getContext(ProjectService);
|
||||||
const project = getContext(Project);
|
const project = getContext(Project);
|
||||||
const platformName = from(platform());
|
const platformName = from(platform());
|
||||||
@ -39,6 +42,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContentWrapper title="Project settings">
|
<ContentWrapper title="Project settings">
|
||||||
|
{#if $baseBranchSwitching}
|
||||||
|
<BaseBranchSwitch />
|
||||||
|
{/if}
|
||||||
<CloudForm />
|
<CloudForm />
|
||||||
<DetailsForm />
|
<DetailsForm />
|
||||||
{#if $platformName != 'win32'}
|
{#if $platformName != 'win32'}
|
||||||
|
53
app/src/routes/settings/experimental/+page.svelte
Normal file
53
app/src/routes/settings/experimental/+page.svelte
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||||
|
import Toggle from '$lib/components/Toggle.svelte';
|
||||||
|
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
|
||||||
|
import {
|
||||||
|
featureBaseBranchSwitching,
|
||||||
|
featureAdvancedCommitOperations
|
||||||
|
} from '$lib/config/uiFeatureFlags';
|
||||||
|
const baseBranchSwitching = featureBaseBranchSwitching();
|
||||||
|
const advancedCommitOperations = featureAdvancedCommitOperations();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContentWrapper title="Experimental features">
|
||||||
|
<p class="text-base-body-13 experimental-settings__text">
|
||||||
|
This sections contains a list of feature flags for features that are still in development or in
|
||||||
|
an experimental stage.
|
||||||
|
</p>
|
||||||
|
<SectionCard labelFor="baseBranchSwitching" orientation="row">
|
||||||
|
<svelte:fragment slot="title">Switching the base branch</svelte:fragment>
|
||||||
|
<svelte:fragment slot="caption">
|
||||||
|
This allows changing of the base branch (trunk) after the initial project setup from within
|
||||||
|
the project settings. Not fully tested yet, use with caution.
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<Toggle
|
||||||
|
id="baseBranchSwitching"
|
||||||
|
checked={$baseBranchSwitching}
|
||||||
|
on:change={() => ($baseBranchSwitching = !$baseBranchSwitching)}
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
|
</SectionCard>
|
||||||
|
<SectionCard labelFor="advancedCommitOperations" orientation="row">
|
||||||
|
<svelte:fragment slot="title">Advanced commit operations</svelte:fragment>
|
||||||
|
<svelte:fragment slot="caption">
|
||||||
|
Allows for reordeing of commits, changing the message as well as undoing of commits anywhere
|
||||||
|
in the stack. In addition it allows for adding an empty commit between two other commits.
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<Toggle
|
||||||
|
id="advancedCommitOperations"
|
||||||
|
checked={$advancedCommitOperations}
|
||||||
|
on:change={() => ($advancedCommitOperations = !$advancedCommitOperations)}
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
|
</SectionCard>
|
||||||
|
</ContentWrapper>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.experimental-settings__text {
|
||||||
|
color: var(--clr-text-2);
|
||||||
|
margin-bottom: var(--size-12);
|
||||||
|
}
|
||||||
|
</style>
|
@ -116,10 +116,36 @@ button {
|
|||||||
|
|
||||||
/* DIALOG STYLES */
|
/* DIALOG STYLES */
|
||||||
|
|
||||||
|
dialog[open] {
|
||||||
|
animation: dialog-zoom 0.25s cubic-bezier(0.34, 1.35, 0.7, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-zoom {
|
||||||
|
from {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
background-color: rgba(214, 214, 214, 0.4);
|
background-color: rgba(214, 214, 214, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog[open]::backdrop {
|
||||||
|
animation: dialog-fade 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-fade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dark dialog::backdrop {
|
.dark dialog::backdrop {
|
||||||
background-color: rgba(0, 0, 0, 0.35);
|
background-color: rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
/* text Base Body Classes */
|
/* text Base Body Classes */
|
||||||
.text-base-body-10 {
|
.text-base-body-10 {
|
||||||
font-family: var(--base-font-family);
|
font-family: var(--base-font-family);
|
||||||
font-size: 625rem;
|
font-size: 0.625rem;
|
||||||
font-weight: var(--base-font-weight);
|
font-weight: var(--base-font-weight);
|
||||||
line-height: var(--text-body-line-height);
|
line-height: var(--text-body-line-height);
|
||||||
}
|
}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "gitbutler-analytics"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
doctest = false
|
|
||||||
test = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gitbutler-core.workspace = true
|
|
||||||
thiserror.workspace = true
|
|
||||||
tracing = "0.1.40"
|
|
||||||
tokio.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
|
|
||||||
|
|
||||||
async-trait = "0.1.79"
|
|
||||||
chrono = { version = "0.4.37", features = ["serde"] }
|
|
||||||
reqwest = { version = "0.12.2", features = ["json"] }
|
|
||||||
|
|
||||||
[lints.clippy]
|
|
||||||
all = "deny"
|
|
||||||
perf = "deny"
|
|
||||||
correctness = "deny"
|
|
@ -1,100 +0,0 @@
|
|||||||
//! A client to provide analytics.
|
|
||||||
use std::{fmt, str, sync::Arc};
|
|
||||||
|
|
||||||
use gitbutler_core::{projects::ProjectId, users::User};
|
|
||||||
|
|
||||||
mod posthog;
|
|
||||||
|
|
||||||
pub struct Config<'c> {
|
|
||||||
pub posthog_token: Option<&'c str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum Event {
|
|
||||||
HeadChange {
|
|
||||||
project_id: ProjectId,
|
|
||||||
reference_name: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Event {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Event::HeadChange {
|
|
||||||
project_id,
|
|
||||||
reference_name,
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"HeadChange(project_id: {}, reference_name: {})",
|
|
||||||
project_id, reference_name
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
|
||||||
pub fn project_id(&self) -> ProjectId {
|
|
||||||
match self {
|
|
||||||
Event::HeadChange { project_id, .. } => *project_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_posthog_event(self, user: &User) -> posthog::Event {
|
|
||||||
match self {
|
|
||||||
Event::HeadChange {
|
|
||||||
project_id,
|
|
||||||
reference_name: reference,
|
|
||||||
} => {
|
|
||||||
let mut event =
|
|
||||||
posthog::Event::new("git::head_changed", &format!("user_{}", user.id));
|
|
||||||
event.insert_prop("project_id", format!("project_{}", project_id));
|
|
||||||
event.insert_prop("reference", reference);
|
|
||||||
event
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NOTE: Needs to be `Clone` only because the watcher wants to obtain it from `tauri`.
|
|
||||||
/// It's just for dependency injection.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Client {
|
|
||||||
client: Arc<dyn posthog::Client + Sync + Send>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
pub fn new(app_name: String, app_version: String, config: &Config) -> Self {
|
|
||||||
let client: Arc<dyn posthog::Client + Sync + Send> =
|
|
||||||
if let Some(posthog_token) = config.posthog_token {
|
|
||||||
let real = posthog::real::Client::new(posthog::real::ClientOptions {
|
|
||||||
api_key: posthog_token.to_string(),
|
|
||||||
app_name,
|
|
||||||
app_version,
|
|
||||||
});
|
|
||||||
let real_with_retry = posthog::retry::Client::new(real);
|
|
||||||
Arc::new(real_with_retry)
|
|
||||||
} else {
|
|
||||||
Arc::<posthog::mock::Client>::default()
|
|
||||||
};
|
|
||||||
Client { client }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send `event` to analytics and associate it with `user` without blocking.
|
|
||||||
pub fn send_non_anonymous_event_nonblocking(&self, user: &User, event: &Event) {
|
|
||||||
let client = self.client.clone();
|
|
||||||
let event = event.clone().into_posthog_event(user);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(error) = client.capture(&[event]).await {
|
|
||||||
tracing::warn!(?error, "failed to send analytics");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Client {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
client: Arc::new(posthog::mock::Client),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
pub mod mock;
|
|
||||||
pub mod real;
|
|
||||||
pub mod retry;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Client {
|
|
||||||
async fn capture(&self, events: &[Event]) -> Result<(), Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("{code}: {message}")]
|
|
||||||
BadRequest { code: u16, message: String },
|
|
||||||
#[error("Connection error: {0}")]
|
|
||||||
Connection(#[from] reqwest::Error),
|
|
||||||
#[error("Serialization error: {0}")]
|
|
||||||
Serialization(#[from] serde_json::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct Event {
|
|
||||||
event: String,
|
|
||||||
properties: Properties,
|
|
||||||
timestamp: Option<NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Properties {
|
|
||||||
distinct_id: String,
|
|
||||||
props: HashMap<String, serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Properties {
|
|
||||||
fn new<S: Into<String>>(distinct_id: S) -> Self {
|
|
||||||
Self {
|
|
||||||
distinct_id: distinct_id.into(),
|
|
||||||
props: HashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert<K: Into<String>, P: Serialize>(&mut self, key: K, prop: P) {
|
|
||||||
let as_json =
|
|
||||||
serde_json::to_value(prop).expect("safe serialization of a analytics property");
|
|
||||||
let _ = self.props.insert(key.into(), as_json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Event {
|
|
||||||
pub fn new<S: Into<String>>(event: S, distinct_id: S) -> Self {
|
|
||||||
Self {
|
|
||||||
event: event.into(),
|
|
||||||
properties: Properties::new(distinct_id),
|
|
||||||
timestamp: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Errors if `prop` fails to serialize
|
|
||||||
pub fn insert_prop<K: Into<String>, P: Serialize>(&mut self, key: K, prop: P) {
|
|
||||||
self.properties.insert(key, prop);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Client;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl super::Client for Client {
|
|
||||||
#[instrument(skip(self), level = "debug")]
|
|
||||||
async fn capture(&self, _events: &[super::Event]) -> Result<(), super::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use reqwest::{header::CONTENT_TYPE, Client as HttpClient};
|
|
||||||
use serde::Serialize;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
const API_ENDPOINT: &str = "https://eu.posthog.com/batch/";
|
|
||||||
const TIMEOUT: &Duration = &Duration::from_millis(800);
|
|
||||||
|
|
||||||
pub struct ClientOptions {
|
|
||||||
pub app_name: String,
|
|
||||||
pub app_version: String,
|
|
||||||
pub api_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Client {
|
|
||||||
options: ClientOptions,
|
|
||||||
client: HttpClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
pub fn new<C: Into<ClientOptions>>(options: C) -> Self {
|
|
||||||
let client = HttpClient::builder().timeout(*TIMEOUT).build().unwrap(); // Unwrap here is as safe as `HttpClient::new`
|
|
||||||
Client {
|
|
||||||
options: options.into(),
|
|
||||||
client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl super::Client for Client {
|
|
||||||
#[instrument(skip(self), level = "debug")]
|
|
||||||
async fn capture(&self, events: &[super::Event]) -> Result<(), super::Error> {
|
|
||||||
let events = events
|
|
||||||
.iter()
|
|
||||||
.map(|event| {
|
|
||||||
let event = &mut event.clone();
|
|
||||||
event
|
|
||||||
.properties
|
|
||||||
.insert("appName", self.options.app_name.clone());
|
|
||||||
event
|
|
||||||
.properties
|
|
||||||
.insert("appVersion", self.options.app_version.clone());
|
|
||||||
Event::from(event)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let batch = Batch {
|
|
||||||
api_key: &self.options.api_key,
|
|
||||||
batch: events.as_slice(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.client
|
|
||||||
.post(API_ENDPOINT)
|
|
||||||
.header(CONTENT_TYPE, "application/json")
|
|
||||||
.body(serde_json::to_string(&batch)?)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status().is_success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(super::Error::BadRequest {
|
|
||||||
code: response.status().as_u16(),
|
|
||||||
message: response.text().await.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Batch<'a> {
|
|
||||||
api_key: &'a str,
|
|
||||||
batch: &'a [Event],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Event {
|
|
||||||
event: String,
|
|
||||||
properties: super::Properties,
|
|
||||||
timestamp: Option<NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&mut super::Event> for Event {
|
|
||||||
fn from(event: &mut super::Event) -> Self {
|
|
||||||
Self {
|
|
||||||
event: event.event.clone(),
|
|
||||||
properties: event.properties.clone(),
|
|
||||||
timestamp: event.timestamp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tracing::instrument;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Client<T: super::Client + Sync> {
|
|
||||||
inner: T,
|
|
||||||
|
|
||||||
/// Events that failed to be sent
|
|
||||||
/// and are waiting to be retried.
|
|
||||||
batch: Arc<Mutex<Vec<super::Event>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: super::Client + Sync> Client<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Client {
|
|
||||||
inner,
|
|
||||||
|
|
||||||
batch: Arc::new(Mutex::new(Vec::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T: super::Client + Sync> super::Client for Client<T> {
|
|
||||||
#[instrument(skip(self), level = "debug")]
|
|
||||||
async fn capture(&self, events: &[super::Event]) -> Result<(), super::Error> {
|
|
||||||
let mut batch = self.batch.lock().await;
|
|
||||||
batch.extend_from_slice(events);
|
|
||||||
if let Err(error) = self.inner.capture(&batch).await {
|
|
||||||
tracing::warn!("Failed to send analytics: {}", error);
|
|
||||||
} else {
|
|
||||||
batch.clear();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
||||||
|
|
||||||
use super::{super::Client, *};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MockClient {
|
|
||||||
sent: Arc<AtomicUsize>,
|
|
||||||
is_failing: Arc<AtomicBool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MockClient {
|
|
||||||
fn new() -> Self {
|
|
||||||
MockClient {
|
|
||||||
sent: Arc::new(AtomicUsize::new(0)),
|
|
||||||
is_failing: Arc::new(AtomicBool::new(false)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_failing(&self, is_failing: bool) {
|
|
||||||
self.is_failing.store(is_failing, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sent(&self) -> usize {
|
|
||||||
self.sent.load(Ordering::SeqCst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl super::super::Client for MockClient {
|
|
||||||
async fn capture(&self, events: &[super::super::Event]) -> Result<(), super::super::Error> {
|
|
||||||
if self.is_failing.load(Ordering::SeqCst) {
|
|
||||||
Err(super::super::Error::BadRequest {
|
|
||||||
code: 400,
|
|
||||||
message: "Bad request".to_string(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
self.sent.fetch_add(events.len(), Ordering::SeqCst);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn retry() {
|
|
||||||
let inner_client = MockClient::new();
|
|
||||||
let retry_client = super::Client::new(inner_client.clone());
|
|
||||||
|
|
||||||
inner_client.set_failing(true);
|
|
||||||
|
|
||||||
retry_client
|
|
||||||
.capture(&[super::super::Event::new("test", "test")])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(inner_client.get_sent(), 0);
|
|
||||||
|
|
||||||
retry_client
|
|
||||||
.capture(&[super::super::Event::new("test", "test")])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(inner_client.get_sent(), 0);
|
|
||||||
|
|
||||||
inner_client.set_failing(false);
|
|
||||||
|
|
||||||
retry_client
|
|
||||||
.capture(&[super::super::Event::new("test", "test")])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(inner_client.get_sent(), 3);
|
|
||||||
|
|
||||||
retry_client
|
|
||||||
.capture(&[super::super::Event::new("test", "test")])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(inner_client.get_sent(), 4);
|
|
||||||
}
|
|
||||||
}
|
|
@ -43,15 +43,15 @@ fn score_ignores_whitespace() {
|
|||||||
assert_score!(sig, "\t\t hel lo\n\two rld \t\t", 1.0);
|
assert_score!(sig, "\t\t hel lo\n\two rld \t\t", 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEXT1: &str = include_str!("../fixtures/text1.txt");
|
const TEXT1: &str = include_str!("fixtures/text1.txt");
|
||||||
const TEXT2: &str = include_str!("../fixtures/text2.txt");
|
const TEXT2: &str = include_str!("fixtures/text2.txt");
|
||||||
const TEXT3: &str = include_str!("../fixtures/text3.txt");
|
const TEXT3: &str = include_str!("fixtures/text3.txt");
|
||||||
const CODE1: &str = include_str!("../fixtures/code1.txt");
|
const CODE1: &str = include_str!("fixtures/code1.txt");
|
||||||
const CODE2: &str = include_str!("../fixtures/code2.txt");
|
const CODE2: &str = include_str!("fixtures/code2.txt");
|
||||||
const CODE3: &str = include_str!("../fixtures/code3.txt");
|
const CODE3: &str = include_str!("fixtures/code3.txt");
|
||||||
const CODE4: &str = include_str!("../fixtures/code4.txt");
|
const CODE4: &str = include_str!("fixtures/code4.txt");
|
||||||
const LARGE1: &str = include_str!("../fixtures/large1.txt");
|
const LARGE1: &str = include_str!("fixtures/large1.txt");
|
||||||
const LARGE2: &str = include_str!("../fixtures/large2.txt");
|
const LARGE2: &str = include_str!("fixtures/large2.txt");
|
||||||
|
|
||||||
macro_rules! real_test {
|
macro_rules! real_test {
|
||||||
($a: ident, $b: ident, are_similar) => {
|
($a: ident, $b: ident, are_similar) => {
|
19
crates/gitbutler-cli/Cargo.toml
Normal file
19
crates/gitbutler-cli/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "gitbutler-cli"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["GitButler <gitbutler@gitbutler.com>"]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "gitbutler-cli"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gitbutler-core.workspace = true
|
||||||
|
clap = "4.5.4"
|
||||||
|
anyhow = "1.0.82"
|
||||||
|
chrono = "0.4.10"
|
||||||
|
|
||||||
|
[target."cfg(unix)".dependencies]
|
||||||
|
pager = "0.16.1"
|
75
crates/gitbutler-cli/src/main.rs
Normal file
75
crates/gitbutler-cli/src/main.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use gitbutler_core::{projects::Project, snapshots::snapshot};
|
||||||
|
|
||||||
|
use clap::{arg, Command};
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
use pager::Pager;
|
||||||
|
|
||||||
|
fn cli() -> Command {
|
||||||
|
Command::new("gitbutler-cli")
|
||||||
|
.about("A CLI tool for GitButler")
|
||||||
|
.arg(arg!(-C <path> "Run as if gitbutler-cli was started in <path> instead of the current working directory."))
|
||||||
|
.subcommand_required(true)
|
||||||
|
.arg_required_else_help(true)
|
||||||
|
.allow_external_subcommands(true)
|
||||||
|
.subcommand(
|
||||||
|
Command::new("snapshot")
|
||||||
|
.about("List and restore snapshots.")
|
||||||
|
.subcommand(Command::new("restore")
|
||||||
|
.about("Restores the state of the working direcory as well as virtual branches to a given snapshot.")
|
||||||
|
.arg(arg!(<SNAPSHOT_ID> "The snapshot to restore"))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
Pager::new().setup();
|
||||||
|
let matches = cli().get_matches();
|
||||||
|
|
||||||
|
let cwd = std::env::current_dir()?.to_string_lossy().to_string();
|
||||||
|
let repo_dir = matches.get_one::<String>("path").unwrap_or(&cwd);
|
||||||
|
|
||||||
|
match matches.subcommand() {
|
||||||
|
Some(("snapshot", sub_matches)) => match sub_matches.subcommand() {
|
||||||
|
Some(("restore", sub_matches)) => {
|
||||||
|
let snapshot_id = sub_matches
|
||||||
|
.get_one::<String>("SNAPSHOT_ID")
|
||||||
|
.expect("required");
|
||||||
|
restore_snapshot(repo_dir, snapshot_id)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
list_snapshots(repo_dir)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_snapshots(repo_dir: &str) -> Result<()> {
|
||||||
|
let project = project_from_path(repo_dir);
|
||||||
|
let snapshots = snapshot::list(&project, 100)?;
|
||||||
|
for snapshot in snapshots {
|
||||||
|
let ts = chrono::DateTime::from_timestamp(snapshot.created_at / 1000, 0);
|
||||||
|
let details = snapshot.details;
|
||||||
|
if let (Some(ts), Some(details)) = (ts, details) {
|
||||||
|
println!("{} {} {}", ts, snapshot.id, details.operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_snapshot(repo_dir: &str, snapshot_id: &str) -> Result<()> {
|
||||||
|
let project = project_from_path(repo_dir);
|
||||||
|
snapshot::restore(&project, snapshot_id.to_owned())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_from_path(repo_dir: &str) -> Project {
|
||||||
|
Project {
|
||||||
|
path: std::path::PathBuf::from(repo_dir),
|
||||||
|
enable_snapshots: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
@ -8,15 +8,17 @@ publish = false
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
once_cell = "1.19"
|
once_cell = "1.19"
|
||||||
pretty_assertions = "1.4"
|
pretty_assertions = "1.4"
|
||||||
|
tempfile = "3.10"
|
||||||
gitbutler-testsupport.workspace = true
|
gitbutler-testsupport.workspace = true
|
||||||
|
gitbutler-git = { workspace = true, features = ["test-askpass-path" ]}
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
toml = "0.8.12"
|
toml = "0.8.12"
|
||||||
anyhow = "1.0.81"
|
anyhow = "1.0.82"
|
||||||
async-trait = "0.1.79"
|
async-trait = "0.1.80"
|
||||||
backtrace = { version = "0.3.71", optional = true }
|
backtrace = { version = "0.3.71", optional = true }
|
||||||
bstr = "1.9.1"
|
bstr = "1.9.1"
|
||||||
chrono = { version = "0.4.37", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
diffy = "0.3.0"
|
diffy = "0.3.0"
|
||||||
filetime = "0.2.23"
|
filetime = "0.2.23"
|
||||||
fslock = "0.2.1"
|
fslock = "0.2.1"
|
||||||
@ -33,7 +35,7 @@ r2d2_sqlite = "0.22.0"
|
|||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
refinery = { version = "0.8", features = [ "rusqlite" ] }
|
refinery = { version = "0.8", features = [ "rusqlite" ] }
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
reqwest = { version = "0.12.2", features = ["json"] }
|
reqwest = { version = "0.12.4", features = ["json"] }
|
||||||
resolve-path = "0.1.0"
|
resolve-path = "0.1.0"
|
||||||
rusqlite.workspace = true
|
rusqlite.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
@ -41,8 +43,9 @@ serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
|
|||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
similar = { version = "2.5.0", features = ["unicode"] }
|
similar = { version = "2.5.0", features = ["unicode"] }
|
||||||
slug = "0.1.5"
|
slug = "0.1.5"
|
||||||
ssh-key = { version = "0.6.5", features = [ "alloc", "ed25519" ] }
|
ssh-key = { version = "0.6.6", features = [ "alloc", "ed25519" ] }
|
||||||
ssh2 = { version = "0.9.4", features = ["vendored-openssl"] }
|
ssh2 = { version = "0.9.4", features = ["vendored-openssl"] }
|
||||||
|
strum = { version = "0.26", features = ["derive"] }
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros" ] }
|
tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros" ] }
|
||||||
@ -52,7 +55,6 @@ urlencoding = "2.1.3"
|
|||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
zip = "0.6.5"
|
zip = "0.6.5"
|
||||||
tempfile = "3.10"
|
|
||||||
gitbutler-git.workspace = true
|
gitbutler-git.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -93,10 +93,7 @@ impl Proxy {
|
|||||||
|
|
||||||
async fn proxy_author(&self, author: Author) -> Author {
|
async fn proxy_author(&self, author: Author) -> Author {
|
||||||
Author {
|
Author {
|
||||||
gravatar_url: self
|
gravatar_url: self.proxy(&author.gravatar_url).await.unwrap_or_else(|error| {
|
||||||
.proxy(&author.gravatar_url)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|error| {
|
|
||||||
tracing::error!(gravatar_url = %author.gravatar_url, ?error, "failed to proxy gravatar url");
|
tracing::error!(gravatar_url = %author.gravatar_url, ?error, "failed to proxy gravatar url");
|
||||||
author.gravatar_url
|
author.gravatar_url
|
||||||
}),
|
}),
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
use std::{
|
use std::fmt::{Display, Formatter};
|
||||||
fmt::{Display, Formatter},
|
|
||||||
time::SystemTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
@ -50,29 +47,20 @@ impl Document {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let operations = operations::get_delta_operations(&self.to_string(), new_text);
|
let operations = operations::get_delta_operations(&self.to_string(), new_text);
|
||||||
let delta = if operations.is_empty() {
|
|
||||||
|
if operations.is_empty() {
|
||||||
if let Some(reader::Content::UTF8(value)) = value {
|
if let Some(reader::Content::UTF8(value)) = value {
|
||||||
if !value.is_empty() {
|
if !value.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delta::Delta {
|
let delta = delta::Delta {
|
||||||
operations,
|
operations,
|
||||||
timestamp_ms: SystemTime::now()
|
timestamp_ms: crate::time::now_ms(),
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delta::Delta {
|
|
||||||
operations,
|
|
||||||
timestamp_ms: SystemTime::now()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis(),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
apply_deltas(&mut self.doc, &vec![delta.clone()])?;
|
apply_deltas(&mut self.doc, &vec![delta.clone()])?;
|
||||||
self.deltas.push(delta.clone());
|
self.deltas.push(delta.clone());
|
||||||
Ok(Some(delta))
|
Ok(Some(delta))
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bstr::BString;
|
use bstr::BString;
|
||||||
use gix::dir::walk::EmissionMode;
|
use gix::dir::walk::EmissionMode;
|
||||||
|
use gix::tempfile::create_dir::Retries;
|
||||||
|
use gix::tempfile::{AutoRemove, ContainingDirectory};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
// Returns an ordered list of relative paths for files inside a directory recursively.
|
// Returns an ordered list of relative paths for files inside a directory recursively.
|
||||||
@ -48,3 +51,71 @@ pub fn iter_worktree_files(
|
|||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.map(|e| e.entry.rela_path))
|
.map(|e| e.entry.rela_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a single file so that the write either fully succeeds, or fully fails,
|
||||||
|
/// assuming the containing directory already exists.
|
||||||
|
pub(crate) fn write<P: AsRef<Path>>(
|
||||||
|
file_path: P,
|
||||||
|
contents: impl AsRef<[u8]>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
Ok(std::fs::write(file_path, contents)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let mut temp_file = gix::tempfile::new(
|
||||||
|
file_path.as_ref().parent().unwrap(),
|
||||||
|
ContainingDirectory::Exists,
|
||||||
|
AutoRemove::Tempfile,
|
||||||
|
)?;
|
||||||
|
temp_file.write_all(contents.as_ref())?;
|
||||||
|
Ok(persist_tempfile(temp_file, file_path)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a single file so that the write either fully succeeds, or fully fails,
|
||||||
|
/// and create all leading directories.
|
||||||
|
pub(crate) fn create_dirs_then_write<P: AsRef<Path>>(
|
||||||
|
file_path: P,
|
||||||
|
contents: impl AsRef<[u8]>,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let dir = file_path.as_ref().parent().unwrap();
|
||||||
|
if !dir.exists() {
|
||||||
|
std::fs::create_dir_all(dir)?;
|
||||||
|
}
|
||||||
|
std::fs::write(file_path, contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let mut temp_file = gix::tempfile::new(
|
||||||
|
file_path.as_ref().parent().unwrap(),
|
||||||
|
ContainingDirectory::CreateAllRaceProof(Retries::default()),
|
||||||
|
AutoRemove::Tempfile,
|
||||||
|
)?;
|
||||||
|
temp_file.write_all(contents.as_ref())?;
|
||||||
|
persist_tempfile(temp_file, file_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persist_tempfile(
|
||||||
|
tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>,
|
||||||
|
to_path: impl AsRef<Path>,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
match tempfile.persist(to_path) {
|
||||||
|
Ok(Some(_opened_file)) => {
|
||||||
|
// EXPERIMENT: Does this fix #3601?
|
||||||
|
#[cfg(windows)]
|
||||||
|
_opened_file.sync_all()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(None) => unreachable!(
|
||||||
|
"BUG: a signal has caused the tempfile to be removed, but we didn't install a handler"
|
||||||
|
),
|
||||||
|
Err(err) => Err(err.error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@ use std::{
|
|||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufReader, Read},
|
io::{BufReader, Read},
|
||||||
path, time,
|
path,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
@ -217,7 +217,8 @@ impl Repository {
|
|||||||
|
|
||||||
// Push to the remote
|
// Push to the remote
|
||||||
remote
|
remote
|
||||||
.push(&[&remote_refspec], Some(&mut push_options)).map_err(|error| match error {
|
.push(&[&remote_refspec], Some(&mut push_options))
|
||||||
|
.map_err(|error| match error {
|
||||||
git::Error::Network(error) => {
|
git::Error::Network(error) => {
|
||||||
tracing::warn!(project_id = %self.project.id, error = %error, "failed to push gb repo");
|
tracing::warn!(project_id = %self.project.id, error = %error, "failed to push gb repo");
|
||||||
RemoteError::Network
|
RemoteError::Network
|
||||||
@ -279,10 +280,7 @@ impl Repository {
|
|||||||
&self,
|
&self,
|
||||||
project_repository: &project_repository::Repository,
|
project_repository: &project_repository::Repository,
|
||||||
) -> Result<sessions::Session> {
|
) -> Result<sessions::Session> {
|
||||||
let now_ms = time::SystemTime::now()
|
let now_ms = crate::time::now_ms();
|
||||||
.duration_since(time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis();
|
|
||||||
|
|
||||||
let meta = match project_repository.get_head() {
|
let meta = match project_repository.get_head() {
|
||||||
Result::Ok(head) => sessions::Meta {
|
Result::Ok(head) => sessions::Meta {
|
||||||
@ -335,10 +333,7 @@ impl Repository {
|
|||||||
|
|
||||||
let updated_session = sessions::Session {
|
let updated_session = sessions::Session {
|
||||||
meta: sessions::Meta {
|
meta: sessions::Meta {
|
||||||
last_timestamp_ms: time::SystemTime::now()
|
last_timestamp_ms: crate::time::now_ms(),
|
||||||
.duration_since(time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis(),
|
|
||||||
..current_session.meta
|
..current_session.meta
|
||||||
},
|
},
|
||||||
..current_session
|
..current_session
|
||||||
|
@ -113,7 +113,8 @@ impl Helper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Self {
|
pub fn from_path(path: impl Into<PathBuf>) -> Self {
|
||||||
|
let path = path.into();
|
||||||
let keys = keys::Controller::from_path(&path);
|
let keys = keys::Controller::from_path(&path);
|
||||||
let users = users::Controller::from_path(path);
|
let users = users::Controller::from_path(path);
|
||||||
let home_dir = std::env::var_os("HOME").map(PathBuf::from);
|
let home_dir = std::env::var_os("HOME").map(PathBuf::from);
|
||||||
|
@ -9,7 +9,6 @@ use tracing::instrument;
|
|||||||
|
|
||||||
use super::Repository;
|
use super::Repository;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::virtual_branches::BranchStatus;
|
|
||||||
|
|
||||||
pub type DiffByPathMap = HashMap<PathBuf, FileDiff>;
|
pub type DiffByPathMap = HashMap<PathBuf, FileDiff>;
|
||||||
|
|
||||||
@ -53,6 +52,7 @@ pub struct GitHunk {
|
|||||||
#[serde(rename = "diff", serialize_with = "crate::serde::as_string_lossy")]
|
#[serde(rename = "diff", serialize_with = "crate::serde::as_string_lossy")]
|
||||||
pub diff_lines: BString,
|
pub diff_lines: BString,
|
||||||
pub binary: bool,
|
pub binary: bool,
|
||||||
|
pub locked_to: Box<[HunkLock]>,
|
||||||
pub change_type: ChangeType,
|
pub change_type: ChangeType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +69,7 @@ impl GitHunk {
|
|||||||
diff_lines: hex_id.into(),
|
diff_lines: hex_id.into(),
|
||||||
binary: true,
|
binary: true,
|
||||||
change_type,
|
change_type,
|
||||||
|
locked_to: Box::new([]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +83,7 @@ impl GitHunk {
|
|||||||
diff_lines: Default::default(),
|
diff_lines: Default::default(),
|
||||||
binary: false,
|
binary: false,
|
||||||
change_type: ChangeType::Modified,
|
change_type: ChangeType::Modified,
|
||||||
|
locked_to: Box::new([]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,6 +93,21 @@ impl GitHunk {
|
|||||||
pub fn contains(&self, line: u32) -> bool {
|
pub fn contains(&self, line: u32) -> bool {
|
||||||
self.new_start <= line && self.new_start + self.new_lines >= line
|
self.new_start <= line && self.new_start + self.new_lines >= line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_locks(mut self, locks: &[HunkLock]) -> Self {
|
||||||
|
self.locked_to = locks.to_owned().into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hunk is locked when it depends on changes in commits that are in your
|
||||||
|
// workspace. A hunk can be locked to more than one branch if it overlaps
|
||||||
|
// with more than one committed hunk.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Copy)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HunkLock {
|
||||||
|
pub branch_id: uuid::Uuid,
|
||||||
|
pub commit_id: git::Oid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Default)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Default)]
|
||||||
@ -298,6 +315,7 @@ fn hunks_by_filepath(repo: Option<&Repository>, diff: &git2::Diff) -> Result<Dif
|
|||||||
diff_lines: line.into_owned(),
|
diff_lines: line.into_owned(),
|
||||||
binary: false,
|
binary: false,
|
||||||
change_type,
|
change_type,
|
||||||
|
locked_to: Box::new([]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LineOrHexHash::HexHashOfBinaryBlob(id) => {
|
LineOrHexHash::HexHashOfBinaryBlob(id) => {
|
||||||
@ -404,12 +422,13 @@ pub fn reverse_hunk(hunk: &GitHunk) -> Option<GitHunk> {
|
|||||||
diff_lines: diff,
|
diff_lines: diff,
|
||||||
binary: hunk.binary,
|
binary: hunk.binary,
|
||||||
change_type: hunk.change_type,
|
change_type: hunk.change_type,
|
||||||
|
locked_to: Box::new([]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(ST): turning this into an iterator will trigger a cascade of changes that
|
pub fn diff_files_into_hunks(
|
||||||
// mean less unnecessary copies. It also leads to `virtual.rs` - 4k SLOC!
|
files: DiffByPathMap,
|
||||||
pub fn diff_files_into_hunks(files: DiffByPathMap) -> BranchStatus {
|
) -> impl Iterator<Item = (PathBuf, Vec<GitHunk>)> {
|
||||||
HashMap::from_iter(files.into_iter().map(|(path, file)| (path, file.hunks)))
|
files.into_iter().map(|(path, file)| (path, file.hunks))
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ pub enum Error {
|
|||||||
Hooks(#[from] git2_hooks::HooksError),
|
Hooks(#[from] git2_hooks::HooksError),
|
||||||
#[error("http error: {0}")]
|
#[error("http error: {0}")]
|
||||||
Http(git2::Error),
|
Http(git2::Error),
|
||||||
|
#[error("blame error: {0}")]
|
||||||
|
Blame(git2::Error),
|
||||||
#[error("checkout error: {0}")]
|
#[error("checkout error: {0}")]
|
||||||
Checkout(git2::Error),
|
Checkout(git2::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -64,6 +64,8 @@ impl FromStr for Refname {
|
|||||||
return Err(Error::NotRemote(value.to_string()));
|
return Err(Error::NotRemote(value.to_string()));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO(ST): use `gix` (which respects refspecs and settings) to do this transformation
|
||||||
|
// Alternatively, `git2` also has support for respecting refspecs.
|
||||||
let value = value.strip_prefix("refs/remotes/").unwrap();
|
let value = value.strip_prefix("refs/remotes/").unwrap();
|
||||||
|
|
||||||
if let Some((remote, branch)) = value.split_once('/') {
|
if let Some((remote, branch)) = value.split_once('/') {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::{io::Write, path::Path, str};
|
use std::{io::Write, path::Path, str};
|
||||||
|
|
||||||
use git2::Submodule;
|
use git2::{BlameOptions, Submodule};
|
||||||
use git2_hooks::HookResult;
|
use git2_hooks::HookResult;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -478,6 +478,24 @@ impl Repository {
|
|||||||
git2_hooks::hooks_post_commit(&self.0, Some(&["../.husky"]))?;
|
git2_hooks::hooks_post_commit(&self.0, Some(&["../.husky"]))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn blame(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
min_line: u32,
|
||||||
|
max_line: u32,
|
||||||
|
oldest_commit: &Oid,
|
||||||
|
newest_commit: &Oid,
|
||||||
|
) -> Result<git2::Blame> {
|
||||||
|
let mut opts = BlameOptions::new();
|
||||||
|
opts.min_line(min_line as usize)
|
||||||
|
.max_line(max_line as usize)
|
||||||
|
.newest_commit(git2::Oid::from(*newest_commit))
|
||||||
|
.oldest_commit(git2::Oid::from(*oldest_commit));
|
||||||
|
self.0
|
||||||
|
.blame_file(path, Some(&mut opts))
|
||||||
|
.map_err(super::Error::Blame)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CheckoutTreeBuidler<'a> {
|
pub struct CheckoutTreeBuidler<'a> {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::{storage::Storage, PrivateKey};
|
use super::{storage::Storage, PrivateKey};
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ impl Controller {
|
|||||||
Self { storage }
|
Self { storage }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Self {
|
pub fn from_path(path: impl Into<PathBuf>) -> Self {
|
||||||
Self::new(Storage::from_path(path))
|
Self::new(Storage::from_path(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,42 +1,40 @@
|
|||||||
use super::PrivateKey;
|
use super::PrivateKey;
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// TODO(ST): get rid of this type, it's more trouble than it's worth.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Storage {
|
pub struct Storage {
|
||||||
storage: storage::Storage,
|
inner: storage::Storage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("IO error: {0}")]
|
#[error(transparent)]
|
||||||
Storage(#[from] storage::Error),
|
Storage(#[from] std::io::Error),
|
||||||
#[error("SSH key error: {0}")]
|
#[error("SSH key error: {0}")]
|
||||||
SSHKey(#[from] ssh_key::Error),
|
SSHKey(#[from] ssh_key::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Storage {
|
impl Storage {
|
||||||
pub fn new(storage: storage::Storage) -> Storage {
|
pub fn new(storage: storage::Storage) -> Storage {
|
||||||
Storage { storage }
|
Storage { inner: storage }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Storage {
|
pub fn from_path(path: impl Into<PathBuf>) -> Storage {
|
||||||
Storage::new(storage::Storage::new(path))
|
Storage::new(storage::Storage::new(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self) -> Result<Option<PrivateKey>, Error> {
|
pub fn get(&self) -> Result<Option<PrivateKey>, Error> {
|
||||||
self.storage
|
let key = self.inner.read("keys/ed25519")?;
|
||||||
.read("keys/ed25519")
|
key.map(|s| s.parse().map_err(Into::into)).transpose()
|
||||||
.map_err(Error::Storage)
|
|
||||||
.and_then(|s| s.map(|s| s.parse().map_err(Error::SSHKey)).transpose())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(ST): see if Key should rather deal with bytes instead for this kind of serialization.
|
||||||
pub fn create(&self, key: &PrivateKey) -> Result<(), Error> {
|
pub fn create(&self, key: &PrivateKey) -> Result<(), Error> {
|
||||||
self.storage
|
self.inner.write("keys/ed25519", &key.to_string())?;
|
||||||
.write("keys/ed25519", &key.to_string())
|
self.inner
|
||||||
.map_err(Error::Storage)?;
|
.write("keys/ed25519.pub", &key.public_key().to_string())?;
|
||||||
self.storage
|
|
||||||
.write("keys/ed25519.pub", &key.public_key().to_string())
|
|
||||||
.map_err(Error::Storage)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,10 @@ pub mod project_repository;
|
|||||||
pub mod projects;
|
pub mod projects;
|
||||||
pub mod reader;
|
pub mod reader;
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
|
pub mod snapshots;
|
||||||
pub mod ssh;
|
pub mod ssh;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
pub mod time;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
pub mod virtual_branches;
|
pub mod virtual_branches;
|
||||||
|
@ -95,6 +95,8 @@ pub fn conflicting_files(repository: &Repository) -> Result<Vec<String>> {
|
|||||||
Ok(reader.lines().map_while(Result::ok).collect())
|
Ok(reader.lines().map_while(Result::ok).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if `path` is conflicting in `repository`, or if `None`, check if there is any conflict.
|
||||||
|
// TODO(ST): Should this not rather check the conflicting state in the index?
|
||||||
pub fn is_conflicting<P: AsRef<Path>>(repository: &Repository, path: Option<P>) -> Result<bool> {
|
pub fn is_conflicting<P: AsRef<Path>>(repository: &Repository, path: Option<P>) -> Result<bool> {
|
||||||
let conflicts_path = repository.git_repository.path().join("conflicts");
|
let conflicts_path = repository.git_repository.path().join("conflicts");
|
||||||
if !conflicts_path.exists() {
|
if !conflicts_path.exists() {
|
||||||
@ -105,6 +107,7 @@ pub fn is_conflicting<P: AsRef<Path>>(repository: &Repository, path: Option<P>)
|
|||||||
let reader = std::io::BufReader::new(file);
|
let reader = std::io::BufReader::new(file);
|
||||||
let mut files = reader.lines().map_ok(PathBuf::from);
|
let mut files = reader.lines().map_ok(PathBuf::from);
|
||||||
if let Some(pathname) = path {
|
if let Some(pathname) = path {
|
||||||
|
// TODO(ST): This shouldn't work on UTF8 strings.
|
||||||
let pathname = pathname.as_ref();
|
let pathname = pathname.as_ref();
|
||||||
|
|
||||||
// check if pathname is one of the lines in conflicts_path file
|
// check if pathname is one of the lines in conflicts_path file
|
||||||
|
@ -76,7 +76,10 @@ impl Repository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if should_set {
|
if should_set {
|
||||||
if let Err(error) = config.set_str("gc.pruneExpire", "never").and_then(|()| config.set_bool("gitbutler.didSetPrune", true)) {
|
if let Err(error) = config
|
||||||
|
.set_str("gc.pruneExpire", "never")
|
||||||
|
.and_then(|()| config.set_bool("gitbutler.didSetPrune", true))
|
||||||
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"failed to set gc.auto to false for repository at {}; cannot disable gc: {}",
|
"failed to set gc.auto to false for repository at {}; cannot disable gc: {}",
|
||||||
project.path.display(),
|
project.path.display(),
|
||||||
@ -175,17 +178,11 @@ impl Repository {
|
|||||||
let branch = self.git_repository.find_branch(&target_branch_refname)?;
|
let branch = self.git_repository.find_branch(&target_branch_refname)?;
|
||||||
let commit_id = branch.peel_to_commit()?.id();
|
let commit_id = branch.peel_to_commit()?.id();
|
||||||
|
|
||||||
let now = std::time::SystemTime::now()
|
let now = crate::time::now_ms();
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
let branch_name = format!("test-push-{now}");
|
||||||
.unwrap_or(std::time::Duration::from_secs(0))
|
|
||||||
.as_millis()
|
|
||||||
.to_string();
|
|
||||||
let branch_name = format!("test-push-{}", now);
|
|
||||||
|
|
||||||
let refname = git::RemoteRefname::from_str(&format!(
|
let refname =
|
||||||
"refs/remotes/{}/{}",
|
git::RemoteRefname::from_str(&format!("refs/remotes/{remote_name}/{branch_name}",))?;
|
||||||
remote_name, branch_name,
|
|
||||||
))?;
|
|
||||||
|
|
||||||
match self.push(
|
match self.push(
|
||||||
&commit_id,
|
&commit_id,
|
||||||
@ -623,6 +620,8 @@ pub enum RemoteError {
|
|||||||
Network,
|
Network,
|
||||||
#[error("authentication failed")]
|
#[error("authentication failed")]
|
||||||
Auth,
|
Auth,
|
||||||
|
#[error("Git failed")]
|
||||||
|
Git(#[from] git::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
@ -638,6 +637,9 @@ impl ErrorWithContext for RemoteError {
|
|||||||
Code::ProjectGitAuth,
|
Code::ProjectGitAuth,
|
||||||
"Project remote authentication error",
|
"Project remote authentication error",
|
||||||
),
|
),
|
||||||
|
RemoteError::Git(_) => {
|
||||||
|
error::Context::new_static(Code::ProjectGitRemote, "Git command failed")
|
||||||
|
}
|
||||||
RemoteError::Other(error) => {
|
RemoteError::Other(error) => {
|
||||||
return error.custom_context_or_root_cause().into();
|
return error.custom_context_or_root_cause().into();
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user