mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-19 07:32:22 +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:
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
|
||||
- run: |
|
||||
cargo build --locked -p gitbutler-git --bins
|
||||
shell: bash
|
||||
|
||||
- run: |
|
||||
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
|
||||
@ -23,7 +27,9 @@ runs:
|
||||
FEATURES: ${{ inputs.features }}
|
||||
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'
|
||||
env:
|
||||
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:
|
||||
using: "composite"
|
||||
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
|
||||
shell: bash
|
||||
run: |
|
||||
|
10
.github/workflows/publish.yaml
vendored
10
.github/workflows/publish.yaml
vendored
@ -29,9 +29,10 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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:
|
||||
- macos-latest # [macOs, x64]
|
||||
- macos-latest-xlarge # [macOs, ARM64]
|
||||
- macos-13 # [macOs, x64]
|
||||
- macos-latest # [macOs, ARM64]
|
||||
- ubuntu-20.04 # [linux, x64]
|
||||
- windows-latest # [windows, x64]
|
||||
|
||||
@ -191,9 +192,10 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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:
|
||||
- macos-latest # [macOs, x64]
|
||||
- macos-latest-xlarge # [macOs, ARM64]
|
||||
- macos-13 # [macOs, x64]
|
||||
- macos-latest # [macOs, ARM64]
|
||||
- ubuntu-20.04 # [linux, x64]
|
||||
- windows-latest # [windows, x64]
|
||||
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-changeset: ${{ steps.filter.outputs.gitbutler-changeset }}
|
||||
gitbutler-git: ${{ steps.filter.outputs.gitbutler-git }}
|
||||
gitbutler-cli: ${{ steps.filter.outputs.gitbutler-cli }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
@ -51,6 +52,9 @@ jobs:
|
||||
gitbutler-git:
|
||||
- *rust
|
||||
- 'crates/gitbutler-git/**'
|
||||
gitbutler-cli:
|
||||
- *rust
|
||||
- 'crates/gitbutler-cli/**'
|
||||
|
||||
lint-node:
|
||||
needs: changes
|
||||
@ -79,7 +83,7 @@ jobs:
|
||||
- uses: ./.github/actions/init-env-node
|
||||
- run: pnpm test
|
||||
|
||||
rust-init:
|
||||
rust-lint:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
@ -89,7 +93,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
- run: cargo fmt --check --all
|
||||
- run: cargo build --locked --all-targets --tests
|
||||
|
||||
rust-docs:
|
||||
needs: changes
|
||||
@ -107,7 +110,7 @@ jobs:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
|
||||
check-gitbutler-tauri:
|
||||
needs: [changes, rust-init]
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.gitbutler-tauri == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
@ -123,6 +126,7 @@ jobs:
|
||||
- [devtools]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
- uses: ./.github/actions/check-crate
|
||||
with:
|
||||
crate: gitbutler-tauri
|
||||
@ -130,7 +134,7 @@ jobs:
|
||||
action: ${{ matrix.action }}
|
||||
|
||||
check-gitbutler-changeset:
|
||||
needs: [changes, rust-init]
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.gitbutler-changeset == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
@ -147,6 +151,7 @@ jobs:
|
||||
- []
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
- uses: ./.github/actions/check-crate
|
||||
with:
|
||||
crate: gitbutler-changeset
|
||||
@ -154,7 +159,7 @@ jobs:
|
||||
action: ${{ matrix.action }}
|
||||
|
||||
check-gitbutler-git:
|
||||
needs: [changes, rust-init]
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.gitbutler-git == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
@ -171,6 +176,7 @@ jobs:
|
||||
- [tokio]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
- uses: ./.github/actions/check-crate
|
||||
with:
|
||||
crate: gitbutler-git
|
||||
@ -178,7 +184,7 @@ jobs:
|
||||
action: ${{ matrix.action }}
|
||||
|
||||
check-gitbutler-core:
|
||||
needs: [changes, rust-init]
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.gitbutler-core == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
@ -194,12 +200,38 @@ jobs:
|
||||
- []
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
- uses: ./.github/actions/check-crate
|
||||
with:
|
||||
crate: gitbutler-core
|
||||
features: ${{ toJson(matrix.features) }}
|
||||
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:
|
||||
if: always()
|
||||
needs:
|
||||
@ -208,7 +240,9 @@ jobs:
|
||||
- check-gitbutler-core
|
||||
- check-gitbutler-changeset
|
||||
- check-gitbutler-git
|
||||
- check-gitbutler-cli
|
||||
- check-rust-windows
|
||||
- rust-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Decide whether the needed jobs succeeded or failed
|
||||
@ -223,6 +257,6 @@ jobs:
|
||||
if: ${{ needs.changes.outputs.rust == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
- 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
|
||||
|
||||
.DS_Store
|
||||
|
||||
.env
|
||||
.env.*
|
||||
|
470
Cargo.lock
generated
470
Cargo.lock
generated
@ -97,14 +97,60 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.81"
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
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]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
@ -300,9 +346,9 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.79"
|
||||
version = "0.1.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681"
|
||||
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -434,6 +480,12 @@ version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
@ -752,9 +804,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.37"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@ -775,6 +827,33 @@ dependencies = [
|
||||
"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]]
|
||||
name = "clru"
|
||||
version = "0.6.1"
|
||||
@ -817,6 +896,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.6"
|
||||
@ -1095,7 +1180,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
@ -1123,16 +1208,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
@ -1415,6 +1490,17 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "errno"
|
||||
version = "0.3.8"
|
||||
@ -1425,6 +1511,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
@ -1598,18 +1694,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "flate2"
|
||||
version = "1.0.28"
|
||||
@ -2031,21 +2115,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "gitbutler-changeset"
|
||||
version = "0.0.0"
|
||||
@ -2054,6 +2123,17 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler-cli"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"gitbutler-core",
|
||||
"pager",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler-core"
|
||||
version = "0.0.0"
|
||||
@ -2084,7 +2164,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"refinery",
|
||||
"regex",
|
||||
"reqwest 0.12.2",
|
||||
"reqwest 0.12.4",
|
||||
"resolve-path",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
@ -2094,6 +2174,7 @@ dependencies = [
|
||||
"slug",
|
||||
"ssh-key",
|
||||
"ssh2",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@ -2131,7 +2212,6 @@ dependencies = [
|
||||
"console-subscriber",
|
||||
"futures",
|
||||
"git2",
|
||||
"gitbutler-analytics",
|
||||
"gitbutler-core",
|
||||
"gitbutler-testsupport",
|
||||
"gitbutler-watcher",
|
||||
@ -2140,9 +2220,7 @@ dependencies = [
|
||||
"nonzero_ext",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"reqwest 0.12.2",
|
||||
"sentry",
|
||||
"sentry-tracing",
|
||||
"reqwest 0.12.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slug",
|
||||
@ -2182,7 +2260,6 @@ dependencies = [
|
||||
"crossbeam-channel",
|
||||
"futures",
|
||||
"git2",
|
||||
"gitbutler-analytics",
|
||||
"gitbutler-core",
|
||||
"gitbutler-testsupport",
|
||||
"itertools 0.12.1",
|
||||
@ -3117,6 +3194,12 @@ version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@ -3147,17 +3230,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "html5ever"
|
||||
version = "0.26.0"
|
||||
@ -3526,6 +3598,12 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.11.0"
|
||||
@ -3860,12 +3938,6 @@ dependencies = [
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -4399,6 +4471,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pango"
|
||||
version = "0.15.10"
|
||||
@ -5291,7 +5373,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@ -5311,11 +5393,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.2"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338"
|
||||
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
@ -5335,7 +5417,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@ -5348,7 +5430,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg 0.50.0",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5496,7 +5578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys 0.3.8",
|
||||
@ -5510,7 +5592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"errno",
|
||||
"errno 0.3.8",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.13",
|
||||
"windows-sys 0.52.0",
|
||||
@ -5525,6 +5607,22 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
@ -5648,140 +5746,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -5790,9 +5768,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.115"
|
||||
version = "1.0.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"itoa 1.0.11",
|
||||
@ -6129,9 +6107,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ssh-key"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b71299a724c8d84956caaf8fc3b3ea57c3587fe2d0b800cd0dc1f3599905d7e"
|
||||
checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"p256",
|
||||
@ -6213,6 +6191,34 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
@ -6274,9 +6280,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.30.8"
|
||||
version = "0.30.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b1a378e48fb3ce3a5cf04359c456c9c98ff689bcf1c1bc6e6a31f247686f275"
|
||||
checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
@ -6417,9 +6423,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "1.6.1"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f078117725e36d55d29fafcbb4b1e909073807ca328ae8deb8c0b3843aac0fed"
|
||||
checksum = "047aefcc7721bfb8024a9bc39d4719112262610502de7a224fa62c4570cd78d4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -6433,7 +6439,7 @@ dependencies = [
|
||||
"glib",
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"http 0.2.12",
|
||||
"ignore",
|
||||
"indexmap 1.9.3",
|
||||
@ -6551,7 +6557,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
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 = [
|
||||
"byte-unit",
|
||||
"fern",
|
||||
@ -6566,7 +6572,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-single-instance"
|
||||
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 = [
|
||||
"log",
|
||||
"serde",
|
||||
@ -6580,7 +6586,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
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 = [
|
||||
"log",
|
||||
"serde",
|
||||
@ -6592,7 +6598,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
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 = [
|
||||
"bincode",
|
||||
"bitflags 2.5.0",
|
||||
@ -6715,18 +6721,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.58"
|
||||
version = "1.0.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
||||
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.58"
|
||||
version = "1.0.59"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
||||
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -7122,15 +7128,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.15"
|
||||
@ -7164,19 +7161,6 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "url"
|
||||
version = "2.5.0"
|
||||
@ -7207,6 +7191,12 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.8.0"
|
||||
|
@ -1,12 +1,12 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/gitbutler-analytics",
|
||||
"crates/gitbutler-core",
|
||||
"crates/gitbutler-tauri",
|
||||
"crates/gitbutler-changeset",
|
||||
"crates/gitbutler-git",
|
||||
"crates/gitbutler-watcher",
|
||||
"crates/gitbutler-testsupport",
|
||||
"crates/gitbutler-cli",
|
||||
]
|
||||
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"] }
|
||||
uuid = { version = "1.8.0", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0.58"
|
||||
thiserror = "1.0.59"
|
||||
rusqlite = { version = "0.29.0", features = [ "bundled", "blob" ] }
|
||||
tokio = { version = "1.37.0", default-features = false }
|
||||
|
||||
gitbutler-git = { path = "crates/gitbutler-git" }
|
||||
gitbutler-core = { path = "crates/gitbutler-core" }
|
||||
gitbutler-analytics = { path = "crates/gitbutler-analytics" }
|
||||
gitbutler-watcher = { path = "crates/gitbutler-watcher" }
|
||||
gitbutler-testsupport = { path = "crates/gitbutler-testsupport" }
|
||||
gitbutler-cli ={ path = "crates/gitbutler-cli" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
lto = true # Enables link to optimizations
|
||||
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
|
||||
on the PATH, and then `npm i -g pnpm`.
|
||||
|
||||
This often causes file permissions. First, the AppData folder may not
|
||||
be present. Be sure to create it if it isn't.
|
||||
Sometimes npm's prefix is incorrect on Windows, we can check this via:
|
||||
|
||||
```
|
||||
mkdir %APPDATA%\npm
|
||||
```sh
|
||||
npm config get prefix
|
||||
```
|
||||
|
||||
Secondly, typically folders within `Program Files` are not writable.
|
||||
You'll need to fix the security permissions for the `nodejs` folder.
|
||||
If it's not `C:\Users\<username>\AppData\Roaming\npm` or another folder that is
|
||||
normally writable, then we can set it in Powershell:
|
||||
|
||||
> **NOTE:** Under specific circumstances, depending on your usage of
|
||||
> Node.js, this may pose a security concern. Be sure to understand
|
||||
> the implications of this before proceeding.
|
||||
```sh
|
||||
mkdir -p $APPDATA\npm
|
||||
npm config set prefix $env:APPDATA\npm
|
||||
```
|
||||
|
||||
1. Right-click on the `nodejs` folder in `Program Files`.
|
||||
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.
|
||||
Afterwards, add this folder to your PATH.
|
||||
|
||||
### 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.
|
||||
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).
|
||||
[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.
|
||||
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",
|
||||
"@octokit/rest": "^20.1.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/kit": "^1.30.4",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"@tauri-apps/api": "^1.5.4",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/diff": "^5.2.0",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/lscache": "^1.3.4",
|
||||
"@types/marked": "^5.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||
"@typescript-eslint/parser": "^7.7.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"class-transformer": "^0.5.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
@ -61,7 +61,7 @@
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"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",
|
||||
"leven": "^4.0.0",
|
||||
"lscache": "^1.3.2",
|
||||
@ -71,14 +71,14 @@
|
||||
"nanoid": "^5.0.7",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-load-config": "^5.0.3",
|
||||
"posthog-js": "1.128.2",
|
||||
"posthog-js": "1.130.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.14",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"svelte": "^4.2.15",
|
||||
"svelte-check": "^3.6.9",
|
||||
"svelte-check": "^3.7.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"svelte-french-toast": "^1.2.0",
|
||||
"svelte-loadable-store": "^2.0.1",
|
||||
@ -95,6 +95,6 @@
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
"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()]);
|
||||
posthog.init(PUBLIC_POSTHOG_API_KEY, {
|
||||
api_host: 'https://eu.posthog.com',
|
||||
disable_session_recording: appName !== 'GitButler', // only record sessions in production
|
||||
disable_session_recording: true,
|
||||
capture_performance: false,
|
||||
request_batching: true,
|
||||
persistence: 'localStorage',
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AnalyticsSettings from './AnalyticsSettings.svelte';
|
||||
import Button from './Button.svelte';
|
||||
import { initAnalyticsIfEnabled } from '$lib/analytics/analytics';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let analyticsConfirmed: Writable<boolean>;
|
||||
@ -17,6 +18,7 @@
|
||||
icon="chevron-right-small"
|
||||
on:click={() => {
|
||||
$analyticsConfirmed = true;
|
||||
initAnalyticsIfEnabled();
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
|
@ -1,22 +1,27 @@
|
||||
<script lang="ts">
|
||||
import InfoMessage from './InfoMessage.svelte';
|
||||
import Link from './Link.svelte';
|
||||
import SectionCard from './SectionCard.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 metricsEnabled = appMetricsEnabled();
|
||||
let updatedTelemetrySettings = false;
|
||||
const nonAnonMetricsEnabled = appNonAnonMetricsEnabled();
|
||||
|
||||
function toggleErrorReporting() {
|
||||
$errorReportingEnabled = !$errorReportingEnabled;
|
||||
updatedTelemetrySettings = true;
|
||||
}
|
||||
|
||||
function toggleMetrics() {
|
||||
$metricsEnabled = !$metricsEnabled;
|
||||
updatedTelemetrySettings = true;
|
||||
}
|
||||
|
||||
function toggleNonAnonMetrics() {
|
||||
$nonAnonMetricsEnabled = !$nonAnonMetricsEnabled;
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -24,7 +29,13 @@
|
||||
<div class="analytics-settings__content">
|
||||
<p class="text-base-body-13 analytics-settings__text">
|
||||
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 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
|
||||
@ -61,13 +72,19 @@
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
|
||||
{#if updatedTelemetrySettings}
|
||||
<InfoMessage>
|
||||
<svelte:fragment slot="content"
|
||||
>Changes will take effect on the next application start.</svelte:fragment
|
||||
>
|
||||
</InfoMessage>
|
||||
{/if}
|
||||
<SectionCard labelFor="nonAnonMetricsEnabledToggle" on:click={toggleMetrics} orientation="row">
|
||||
<svelte:fragment slot="title">Non-anonymous usage metrics</svelte:fragment>
|
||||
<svelte:fragment slot="caption"
|
||||
>Toggle sharing of identifiable usage statistics.</svelte:fragment
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
id="nonAnonMetricsEnabledToggle"
|
||||
checked={$nonAnonMetricsEnabled}
|
||||
on:change={toggleNonAnonMetrics}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</SectionCard>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
export let help = '';
|
||||
</script>
|
||||
|
||||
<div class="badge text-base-10 text-semibold" use:tooltip={help}>
|
||||
<div class="badge text-base-10 text-bold" use:tooltip={help}>
|
||||
{count}
|
||||
</div>
|
||||
|
||||
@ -15,12 +15,12 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: var(--size-16);
|
||||
min-width: var(--size-16);
|
||||
border-radius: var(--size-16);
|
||||
height: var(--size-14);
|
||||
min-width: var(--size-14);
|
||||
border-radius: var(--size-14);
|
||||
padding: 0 var(--size-4);
|
||||
color: var(--clr-scale-ntrl-100);
|
||||
background-color: var(--clr-scale-ntrl-50);
|
||||
background-color: var(--clr-scale-ntrl-40);
|
||||
line-height: 90%;
|
||||
}
|
||||
</style>
|
||||
|
@ -48,7 +48,7 @@
|
||||
</Button>
|
||||
<div class="commits-list">
|
||||
{#each base.upstreamCommits as commit}
|
||||
<CommitCard {commit} commitUrl={base.commitUrl(commit.id)} />
|
||||
<CommitCard {commit} isUnapplied={true} commitUrl={base.commitUrl(commit.id)} />
|
||||
{/each}
|
||||
</div>
|
||||
<Spacer margin={2} />
|
||||
@ -62,7 +62,7 @@
|
||||
Local
|
||||
</h1>
|
||||
{#each base.recentCommits as commit}
|
||||
<CommitCard {commit} commitUrl={base.commitUrl(commit.id)} />
|
||||
<CommitCard {commit} isUnapplied={true} commitUrl={base.commitUrl(commit.id)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -102,7 +102,7 @@
|
||||
}
|
||||
.row_1 {
|
||||
display: flex;
|
||||
gap: var(--size-6);
|
||||
gap: var(--size-4);
|
||||
align-items: center;
|
||||
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 baseBranch = getContextStore(BaseBranch);
|
||||
const project = getContext(Project);
|
||||
|
||||
const activeBranchesError = vbranchService.activeBranchesError;
|
||||
const activeBranches = vbranchService.activeBranches;
|
||||
|
||||
@ -27,12 +26,16 @@
|
||||
|
||||
let dragHandle: any;
|
||||
let clone: any;
|
||||
|
||||
let isSwitching = false;
|
||||
</script>
|
||||
|
||||
{#if $activeBranchesError}
|
||||
<div class="p-4" data-tauri-drag-region>Something went wrong...</div>
|
||||
{:else if !$activeBranches}
|
||||
<FullviewLoading />
|
||||
{:else if isSwitching}
|
||||
<div class="middle-message">switching base branch...</div>
|
||||
{:else}
|
||||
<div
|
||||
class="board"
|
||||
@ -202,6 +205,12 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-16);
|
||||
}
|
||||
|
||||
.branch {
|
||||
height: 100%;
|
||||
}
|
||||
@ -255,6 +264,38 @@
|
||||
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 {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
@ -41,7 +41,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--clr-theme-container-light);
|
||||
background: var(--clr-bg-1);
|
||||
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||
padding: 0 var(--size-14) var(--size-14);
|
||||
}
|
||||
|
@ -88,7 +88,7 @@
|
||||
branchName={branch.upstreamName ?? branchName}
|
||||
{isUnapplied}
|
||||
{hasIntegratedCommits}
|
||||
remoteExists={!!branch.upstreamName}
|
||||
remoteExists={!!branch.upstream}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
{#if branch.selectedForChanges}
|
||||
@ -118,7 +118,7 @@
|
||||
branchName={branch.upstreamName ?? branchName}
|
||||
{isUnapplied}
|
||||
{hasIntegratedCommits}
|
||||
remoteExists={!!branch.upstreamName}
|
||||
remoteExists={!!branch.upstream}
|
||||
isLaneCollapsed={$isLaneCollapsed}
|
||||
/>
|
||||
|
||||
|
@ -61,11 +61,9 @@
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.branch-name-mesure-el {
|
||||
pointer-events: auto;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
border: 2px solid transparent;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
color: black;
|
||||
position: fixed;
|
||||
display: inline-block;
|
||||
|
@ -42,6 +42,10 @@
|
||||
async function setAIConfigurationValid(user: User | undefined) {
|
||||
aiConfigurationValid = await aiService.validateConfiguration(user?.access_token);
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
@ -52,7 +56,7 @@
|
||||
label="Unapply"
|
||||
on:click={() => {
|
||||
if (branch.id) branchController.unapplyBranch(branch.id);
|
||||
visible = false;
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@ -69,7 +73,7 @@
|
||||
} else {
|
||||
deleteBranchModal.show(branch);
|
||||
}
|
||||
visible = false;
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -77,7 +81,7 @@
|
||||
label="Generate branch name"
|
||||
on:click={() => {
|
||||
dispatch('action', 'generate-branch-name');
|
||||
visible = false;
|
||||
close();
|
||||
}}
|
||||
disabled={isUnapplied ||
|
||||
!($aiGenEnabled && aiConfigurationValid) ||
|
||||
@ -91,7 +95,7 @@
|
||||
disabled={isUnapplied || hasIntegratedCommits}
|
||||
on:click={() => {
|
||||
newRemoteName = branch.upstreamName || normalizeBranchName(branch.name) || '';
|
||||
visible = false;
|
||||
close();
|
||||
renameRemoteModal.show(branch);
|
||||
}}
|
||||
/>
|
||||
@ -101,7 +105,7 @@
|
||||
label="Create branch to the left"
|
||||
on:click={() => {
|
||||
branchController.createBranch({ order: branch.order });
|
||||
visible = false;
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -109,7 +113,7 @@
|
||||
label="Create branch to the right"
|
||||
on:click={() => {
|
||||
branchController.createBranch({ order: branch.order + 1 });
|
||||
visible = false;
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
@ -122,7 +126,6 @@
|
||||
on:submit={() => {
|
||||
branchController.updateBranchRemoteName(branch.id, newRemoteName);
|
||||
renameRemoteModal.close();
|
||||
visible = false;
|
||||
}}
|
||||
>
|
||||
<svelte:fragment>
|
||||
@ -136,9 +139,9 @@
|
||||
</Modal>
|
||||
|
||||
<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.
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls" let:close let:item={branch}>
|
||||
<Button style="ghost" kind="solid" on:click={close}>Cancel</Button>
|
||||
<Button
|
||||
@ -146,7 +149,7 @@
|
||||
kind="solid"
|
||||
on:click={async () => {
|
||||
await branchController.deleteBranch(branch.id);
|
||||
visible = false;
|
||||
deleteBranchModal.close();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
|
@ -12,7 +12,7 @@
|
||||
export let id: string | undefined = undefined;
|
||||
export let loading = false;
|
||||
export let tabindex: number | undefined = undefined;
|
||||
export let type: 'submit' | 'reset' | undefined = undefined;
|
||||
export let type: 'submit' | 'reset' | 'button' | undefined = undefined;
|
||||
// Layout props
|
||||
export let reversedDirection: boolean = false;
|
||||
export let width: number | undefined = undefined;
|
||||
@ -102,39 +102,7 @@
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
|
||||
&.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);
|
||||
}
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.wide {
|
||||
display: flex;
|
||||
@ -238,7 +206,7 @@
|
||||
|
||||
.pop {
|
||||
&.soft {
|
||||
--btn-clr: var(--clr-theme-pop-on-container);
|
||||
--btn-clr: var(--clr-theme-pop-on-soft);
|
||||
--btn-bg: var(--clr-scale-pop-80);
|
||||
/* if button */
|
||||
&:not(.not-clickable, &:disabled):hover {
|
||||
@ -261,7 +229,7 @@
|
||||
|
||||
.success {
|
||||
&.soft {
|
||||
--btn-clr: var(--clr-theme-succ-on-container);
|
||||
--btn-clr: var(--clr-theme-succ-on-soft);
|
||||
--btn-bg: var(--clr-scale-succ-80);
|
||||
/* if button */
|
||||
&:not(.not-clickable, &:disabled):hover {
|
||||
@ -284,7 +252,7 @@
|
||||
|
||||
.error {
|
||||
&.soft {
|
||||
--btn-clr: var(--clr-theme-err-on-container);
|
||||
--btn-clr: var(--clr-theme-err-on-soft);
|
||||
--btn-bg: var(--clr-scale-err-80);
|
||||
/* if button */
|
||||
&:not(.not-clickable, &:disabled):hover {
|
||||
@ -307,7 +275,7 @@
|
||||
|
||||
.warning {
|
||||
&.soft {
|
||||
--btn-clr: var(--clr-theme-warn-on-container);
|
||||
--btn-clr: var(--clr-theme-warn-on-soft);
|
||||
--btn-bg: var(--clr-scale-warn-80);
|
||||
/* if button */
|
||||
&:not(.not-clickable, &:disabled):hover {
|
||||
@ -330,7 +298,7 @@
|
||||
|
||||
.purple {
|
||||
&.soft {
|
||||
--btn-clr: var(--clr-theme-purp-on-container);
|
||||
--btn-clr: var(--clr-theme-purp-on-soft);
|
||||
--btn-bg: var(--clr-scale-purp-80);
|
||||
/* if button */
|
||||
&:not(.not-clickable, &:disabled):hover {
|
||||
|
@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
import BranchFilesList from './BranchFilesList.svelte';
|
||||
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 TimeAgo from '$lib/components/TimeAgo.svelte';
|
||||
import { persistedCommitMessage } from '$lib/config/config';
|
||||
import { featureAdvancedCommitOperations } from '$lib/config/uiFeatureFlags';
|
||||
import { draggable } from '$lib/dragging/draggable';
|
||||
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
@ -26,6 +30,7 @@
|
||||
const project = getContext(Project);
|
||||
const selectedFiles = getSelectedFiles();
|
||||
const fileIdSelection = getContext(FileIdSelection);
|
||||
const advancedCommitOperations = featureAdvancedCommitOperations();
|
||||
|
||||
const commitStore = createCommitStore(commit);
|
||||
$: commitStore.set(commit);
|
||||
@ -47,6 +52,7 @@
|
||||
|
||||
function toggleFiles() {
|
||||
showFiles = !showFiles;
|
||||
|
||||
if (showFiles) loadFiles();
|
||||
}
|
||||
|
||||
@ -56,22 +62,76 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resetHeadCommit() {
|
||||
function undoCommit(commit: Commit | RemoteCommit) {
|
||||
if (!branch || !$baseBranch) {
|
||||
console.error('Unable to reset head commit');
|
||||
console.error('Unable to undo commit');
|
||||
return;
|
||||
}
|
||||
if (branch.commits.length > 1) {
|
||||
branchController.resetBranch(branch.id, branch.commits[1].id);
|
||||
} else if (branch.commits.length === 1 && $baseBranch) {
|
||||
branchController.resetBranch(branch.id, $baseBranch.baseSha);
|
||||
}
|
||||
branchController.undoCommit(branch.id, commit.id);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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>
|
||||
|
||||
<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
|
||||
use:draggable={commit instanceof Commit
|
||||
? {
|
||||
@ -83,30 +143,58 @@
|
||||
>
|
||||
<div class="commit__header" on:click={toggleFiles} on:keyup={onKeyup} role="button" tabindex="0">
|
||||
<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">
|
||||
<span class="commit__title text-semibold text-base-12" class:truncate={!showFiles}>
|
||||
{commit.descriptionTitle}
|
||||
</span>
|
||||
{#if isUndoable && !showFiles}
|
||||
<Tag
|
||||
style="ghost"
|
||||
kind="solid"
|
||||
icon="undo-small"
|
||||
clickable
|
||||
on:click={(e) => {
|
||||
currentCommitMessage.set(commit.description);
|
||||
e.stopPropagation();
|
||||
resetHeadCommit();
|
||||
}}>Undo</Tag
|
||||
>
|
||||
{#if isUndoable}
|
||||
{#if commit.descriptionTitle}
|
||||
<span class="commit__title text-semibold text-base-12" class:truncate={!showFiles}>
|
||||
{commit.descriptionTitle}
|
||||
</span>
|
||||
{: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
|
||||
style="ghost"
|
||||
kind="solid"
|
||||
icon="undo-small"
|
||||
clickable
|
||||
on:click={(e) => {
|
||||
currentCommitMessage.set(commit.description);
|
||||
e.stopPropagation();
|
||||
undoCommit(commit);
|
||||
}}>Undo</Tag
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="commit__title text-base-12" class:truncate={!showFiles}>
|
||||
{commit.descriptionTitle}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showFiles && commit.descriptionBody}
|
||||
<div class="commit__row" transition:slide={{ duration: 100 }}>
|
||||
<span class="commit__body text-base-body-12">
|
||||
{commit.descriptionBody}
|
||||
</span>
|
||||
</div>
|
||||
{#if showFiles}
|
||||
{#if commit.descriptionBody}
|
||||
<div class="commit__row" transition:slide={{ duration: 100 }}>
|
||||
<span class="commit__body text-base-body-12">
|
||||
{commit.descriptionBody}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $advancedCommitOperations && isUndoable}
|
||||
<Tag clickable on:click={openCommitMessageModal}>Edit</Tag>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="commit__row">
|
||||
@ -130,12 +218,50 @@
|
||||
|
||||
{#if showFiles}
|
||||
<div class="files-container" transition:slide={{ duration: 100 }}>
|
||||
<BranchFilesList {files} {isUnapplied} readonly />
|
||||
<BranchFilesList {files} {isUnapplied} />
|
||||
</div>
|
||||
|
||||
{#if hasCommitUrl || isUndoable}
|
||||
<div class="files__footer">
|
||||
{#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
|
||||
style="ghost"
|
||||
kind="solid"
|
||||
@ -144,7 +270,7 @@
|
||||
on:click={(e) => {
|
||||
currentCommitMessage.set(commit.description);
|
||||
e.stopPropagation();
|
||||
resetHeadCommit();
|
||||
undoCommit(commit);
|
||||
}}>Undo</Tag
|
||||
>
|
||||
{/if}
|
||||
@ -221,6 +347,11 @@
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
width: 100%;
|
||||
}
|
||||
.commit__title_no_desc {
|
||||
flex: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.commit__body {
|
||||
flex: 1;
|
||||
@ -237,6 +368,21 @@
|
||||
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 {
|
||||
display: block;
|
||||
flex: 1;
|
||||
@ -268,6 +414,7 @@
|
||||
.files__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-8);
|
||||
padding: var(--size-14);
|
||||
background-color: var(--clr-bg-1);
|
||||
|
@ -1,80 +1,31 @@
|
||||
<script lang="ts">
|
||||
import Button from './Button.svelte';
|
||||
import { AIService } from '$lib/ai/service';
|
||||
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,
|
||||
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 CommitMessageInput from '$lib/components/CommitMessageInput.svelte';
|
||||
import { projectRunCommitHooks, persistedCommitMessage } from '$lib/config/config';
|
||||
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 { Ownership } from '$lib/vbranches/ownership';
|
||||
import { Branch, type LocalFile } from '$lib/vbranches/types';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { Branch } from '$lib/vbranches/types';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly, slide } from 'svelte/transition';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
const aiService = getContext(AIService);
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
action: 'generate-branch-name';
|
||||
}>();
|
||||
|
||||
export let projectId: string;
|
||||
export let expanded: Writable<boolean>;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const selectedOwnership = getContextStore(Ownership);
|
||||
const branch = getContextStore(Branch);
|
||||
const user = getContextStore(User);
|
||||
|
||||
const aiGenEnabled = projectAiGenEnabled(projectId);
|
||||
const runCommitHooks = projectRunCommitHooks(projectId);
|
||||
const commitMessage = persistedCommitMessage(projectId, $branch.id);
|
||||
const commitGenerationExtraConcise = projectCommitGenerationExtraConcise(projectId);
|
||||
const commitGenerationUseEmojis = projectCommitGenerationUseEmojis(projectId);
|
||||
|
||||
let isCommitting = false;
|
||||
let aiLoading = false;
|
||||
|
||||
let contextMenu: ContextMenu;
|
||||
|
||||
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);
|
||||
}
|
||||
let commitMessageValid = false;
|
||||
|
||||
async function commit() {
|
||||
const message = concatMessage(title, description);
|
||||
const message = $commitMessage;
|
||||
isCommitting = true;
|
||||
try {
|
||||
await branchController.commitBranch(
|
||||
@ -88,158 +39,16 @@
|
||||
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>
|
||||
|
||||
<div class="commit-box" class:commit-box__expanded={$expanded}>
|
||||
{#if $expanded}
|
||||
<div class="commit-box__expander" transition:slide={{ duration: 150, easing: quintOut }}>
|
||||
<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 ((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>
|
||||
<CommitMessageInput
|
||||
bind:commitMessage={$commitMessage}
|
||||
bind:valid={commitMessageValid}
|
||||
{commit}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
@ -260,7 +69,7 @@
|
||||
kind="solid"
|
||||
grow
|
||||
loading={isCommitting}
|
||||
disabled={(isCommitting || !title || $selectedOwnership.isEmpty()) && $expanded}
|
||||
disabled={(isCommitting || !commitMessageValid || $selectedOwnership.isEmpty()) && $expanded}
|
||||
id="commit-to-branch"
|
||||
on:click={() => {
|
||||
if ($expanded) {
|
||||
@ -292,57 +101,6 @@
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
|
@ -6,8 +6,15 @@
|
||||
import { dropzone } from '$lib/dragging/dropzone';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { filesToOwnership } from '$lib/vbranches/ownership';
|
||||
import { RemoteCommit, Branch, type Commit, BaseBranch } from '$lib/vbranches/types';
|
||||
import { filesToOwnership, filesToSimpleOwnership } from '$lib/vbranches/ownership';
|
||||
import {
|
||||
RemoteCommit,
|
||||
Branch,
|
||||
type Commit,
|
||||
BaseBranch,
|
||||
LocalFile,
|
||||
RemoteFile
|
||||
} from '$lib/vbranches/types';
|
||||
|
||||
export let commit: Commit | RemoteCommit;
|
||||
export let isHeadCommit: boolean;
|
||||
@ -32,11 +39,6 @@
|
||||
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) {
|
||||
return true;
|
||||
} else if (data instanceof DraggableFile && data.branchId == $branch.id) {
|
||||
@ -47,14 +49,25 @@
|
||||
};
|
||||
}
|
||||
|
||||
function onAmend(data: DraggableFile | DraggableHunk) {
|
||||
if (data instanceof DraggableHunk) {
|
||||
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
|
||||
branchController.amendBranch($branch.id, newOwnership);
|
||||
} else if (data instanceof DraggableFile) {
|
||||
const newOwnership = filesToOwnership(data.files);
|
||||
branchController.amendBranch($branch.id, newOwnership);
|
||||
}
|
||||
function onAmend(commit: Commit | RemoteCommit) {
|
||||
return (data: any) => {
|
||||
if (data instanceof DraggableHunk) {
|
||||
const newOwnership = `${data.hunk.filePath}:${data.hunk.id}`;
|
||||
branchController.amendBranch($branch.id, commit.id, newOwnership);
|
||||
} 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);
|
||||
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) {
|
||||
@ -104,7 +117,7 @@
|
||||
active: 'amend-dz-active',
|
||||
hover: 'amend-dz-hover',
|
||||
accepts: acceptAmend(commit),
|
||||
onDrop: onAmend
|
||||
onDrop: onAmend(commit)
|
||||
}}
|
||||
use:dropzone={{
|
||||
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 { computeAddedRemovedByHunk } from '$lib/utils/metrics';
|
||||
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 type { HunkSection, ContentSection } from '$lib/utils/fileSections';
|
||||
|
||||
@ -21,6 +21,9 @@
|
||||
$: minWidth = getGutterMinWidth(maxLineNumber);
|
||||
|
||||
const localCommits = isFileLocked ? getLocalCommits() : undefined;
|
||||
const remoteCommits = isFileLocked ? getRemoteCommits() : undefined;
|
||||
|
||||
const commits = isFileLocked ? ($localCommits || []).concat($remoteCommits || []) : undefined;
|
||||
let alwaysShow = false;
|
||||
|
||||
function getGutterMinWidth(max: number) {
|
||||
@ -52,10 +55,10 @@
|
||||
<div class="indicators text-base-11">
|
||||
<span class="added">+{added}</span>
|
||||
<span class="removed">-{removed}</span>
|
||||
{#if section.hunk.lockedTo && $localCommits}
|
||||
{#if section.hunk.lockedTo && section.hunk.lockedTo.length > 0 && commits}
|
||||
<div
|
||||
use:tooltip={{
|
||||
text: getLockText(section.hunk.lockedTo, $localCommits),
|
||||
text: getLockText(section.hunk.lockedTo, commits),
|
||||
delay: 500
|
||||
}}
|
||||
>
|
||||
|
@ -111,7 +111,7 @@
|
||||
}
|
||||
}}
|
||||
use:draggable={{
|
||||
data: new DraggableFile($branch?.id || '', file, selectedFiles),
|
||||
data: new DraggableFile($branch?.id || '', file, $commit, selectedFiles),
|
||||
disabled: readonly || isUnapplied,
|
||||
viewportId: 'board-viewport',
|
||||
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 {
|
||||
background-color: var(--clr-theme-err-container);
|
||||
background-color: var(--clr-theme-err-bg);
|
||||
}
|
||||
|
||||
&.pop {
|
||||
background-color: var(--clr-theme-pop-container);
|
||||
background-color: var(--clr-theme-pop-bg);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: var(--clr-theme-warn-container);
|
||||
background-color: var(--clr-theme-warn-bg);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: var(--clr-theme-succ-container);
|
||||
background-color: var(--clr-theme-succ-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,59 +1,102 @@
|
||||
<script lang="ts">
|
||||
import Overlay from './Overlay.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';
|
||||
|
||||
export function show(newItem?: any) {
|
||||
item = newItem;
|
||||
modal.show();
|
||||
}
|
||||
export function close() {
|
||||
item = undefined;
|
||||
modal.close();
|
||||
}
|
||||
let dialog: HTMLDialogElement;
|
||||
let item: any;
|
||||
let open = false;
|
||||
|
||||
export let width: 'default' | 'small' | 'large' = 'default';
|
||||
export let title: string | undefined = undefined;
|
||||
export let icon: keyof typeof iconsJson | undefined = undefined;
|
||||
export let hoverText: string | undefined = undefined;
|
||||
|
||||
let item: any;
|
||||
let modal: Overlay;
|
||||
export function show(newItem?: any) {
|
||||
item = newItem;
|
||||
dialog.showModal();
|
||||
open = true;
|
||||
}
|
||||
|
||||
export function close() {
|
||||
item = undefined;
|
||||
dialog.close();
|
||||
open = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.body.appendChild(dialog);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Overlay bind:this={modal} let:close on:close {width}>
|
||||
<form on:submit>
|
||||
{#if title}
|
||||
<div class="modal__header">
|
||||
<div class="modal__header__content" class:adjust-header={$$slots.header_controls}>
|
||||
{#if icon}
|
||||
<Icon name={icon} />
|
||||
<dialog
|
||||
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}
|
||||
<div class="modal__header">
|
||||
{#if icon}
|
||||
<Icon name={icon} />
|
||||
{/if}
|
||||
<h2 class="text-base-14 text-semibold">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
<h2 class="text-base-14 text-semibold" title={hoverText}>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
{#if $$slots.header_controls}
|
||||
<div class="modal__header__actions">
|
||||
<slot name="header_controls" />
|
||||
|
||||
<div class="modal__body custom-scrollbar">
|
||||
<slot {item} {close} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="modal__body custom-scrollbar">
|
||||
<slot {item} {close} />
|
||||
</div>
|
||||
|
||||
{#if $$slots.controls}
|
||||
<div class="modal__footer">
|
||||
<slot name="controls" {item} {close} />
|
||||
{#if $$slots.controls}
|
||||
<div class="modal__footer">
|
||||
<slot name="controls" {item} {close} />
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</Overlay>
|
||||
</OutClick>
|
||||
{/if}
|
||||
</dialog>
|
||||
|
||||
<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 {
|
||||
display: flex;
|
||||
padding: var(--size-16);
|
||||
@ -61,17 +104,6 @@
|
||||
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 {
|
||||
overflow: auto;
|
||||
padding: var(--size-16);
|
||||
@ -86,8 +118,4 @@
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
background-color: var(--clr-bg-1);
|
||||
}
|
||||
|
||||
.adjust-header {
|
||||
margin-top: var(--size-6);
|
||||
}
|
||||
</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);
|
||||
gap: var(--size-12);
|
||||
padding: var(--size-20);
|
||||
background-color: var(--clr-theme-err-container);
|
||||
background-color: var(--clr-theme-err-bg);
|
||||
border-radius: var(--radius-m);
|
||||
margin-bottom: var(--size-12);
|
||||
}
|
||||
|
@ -122,11 +122,11 @@
|
||||
function getChecksCount(status: ChecksStatus): string {
|
||||
if (!status) return 'Running checks';
|
||||
|
||||
const completed = status.completed || 0;
|
||||
const skipped = status.skipped || 0;
|
||||
const total = (status.totalCount || 0) - skipped;
|
||||
const queued = total - (status.queued || 0);
|
||||
|
||||
return `Running checks ${queued}/${total}`;
|
||||
return `Checks completed ${completed}/${total}`;
|
||||
}
|
||||
|
||||
function getChecksTagInfo(
|
||||
|
@ -48,7 +48,9 @@
|
||||
reversedDirection
|
||||
loading={isDeleting}
|
||||
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>
|
||||
</svelte:fragment>
|
||||
|
@ -93,11 +93,11 @@
|
||||
}
|
||||
|
||||
.success {
|
||||
background: var(--clr-theme-pop-container);
|
||||
background: var(--clr-theme-pop-bg);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--clr-theme-warn-container);
|
||||
background: var(--clr-theme-warn-bg);
|
||||
}
|
||||
.extra-padding {
|
||||
padding: var(--size-20);
|
||||
|
@ -34,12 +34,6 @@
|
||||
dispatch('select', { value });
|
||||
listOpen = false;
|
||||
}
|
||||
|
||||
function scrollIntoView() {
|
||||
const selected = element.querySelector('.selected');
|
||||
if (selected) selected.scrollIntoView();
|
||||
}
|
||||
|
||||
function setMaxHeight() {
|
||||
maxHeight = window.innerHeight - element.getBoundingClientRect().bottom - maxPadding;
|
||||
}
|
||||
@ -52,7 +46,6 @@
|
||||
function openList() {
|
||||
setMaxHeight();
|
||||
listOpen = true;
|
||||
setTimeout(() => scrollIntoView(), 50);
|
||||
}
|
||||
|
||||
function closeList() {
|
||||
|
@ -59,4 +59,12 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--clr-bg-2);
|
||||
|
||||
& .label {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -44,7 +44,7 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
.success.setup-feature {
|
||||
background: var(--clr-theme-pop-container, #f3fcfb);
|
||||
background: var(--clr-theme-pop-bg);
|
||||
}
|
||||
|
||||
.setup-feature__content {
|
||||
|
@ -40,7 +40,7 @@
|
||||
color: var(--clr-scale-ntrl-0);
|
||||
gap: var(--size-12);
|
||||
padding: var(--size-20);
|
||||
background-color: var(--clr-theme-err-container);
|
||||
background-color: var(--clr-theme-err-bg);
|
||||
border-radius: var(--radius-m);
|
||||
margin-bottom: var(--size-12);
|
||||
}
|
||||
|
@ -114,7 +114,7 @@
|
||||
|
||||
.pop {
|
||||
&.soft {
|
||||
color: var(--clr-theme-pop-on-container);
|
||||
color: var(--clr-theme-pop-on-soft);
|
||||
background: var(--clr-scale-pop-80);
|
||||
/* if button */
|
||||
&:not(.not-button, &:disabled):hover {
|
||||
@ -134,7 +134,7 @@
|
||||
|
||||
.success {
|
||||
&.soft {
|
||||
color: var(--clr-theme-succ-on-container);
|
||||
color: var(--clr-theme-succ-on-soft);
|
||||
background: var(--clr-scale-succ-80);
|
||||
/* if button */
|
||||
&:not(.not-button, &:disabled):hover {
|
||||
@ -154,7 +154,7 @@
|
||||
|
||||
.error {
|
||||
&.soft {
|
||||
color: var(--clr-theme-err-on-container);
|
||||
color: var(--clr-theme-err-on-soft);
|
||||
background: var(--clr-scale-err-80);
|
||||
/* if button */
|
||||
&:not(.not-button, &:disabled):hover {
|
||||
@ -174,7 +174,7 @@
|
||||
|
||||
.warning {
|
||||
&.soft {
|
||||
color: var(--clr-theme-warn-on-container);
|
||||
color: var(--clr-theme-warn-on-soft);
|
||||
background: var(--clr-scale-warn-80);
|
||||
/* if button */
|
||||
&:not(.not-button, &:disabled):hover {
|
||||
@ -194,7 +194,7 @@
|
||||
|
||||
.purple {
|
||||
&.soft {
|
||||
color: var(--clr-theme-purp-on-container);
|
||||
color: var(--clr-theme-purp-on-soft);
|
||||
background: var(--clr-scale-purp-80);
|
||||
/* if button */
|
||||
&:not(.not-button, &:disabled):hover {
|
||||
@ -214,43 +214,15 @@
|
||||
|
||||
/* modifiers */
|
||||
|
||||
.not-button {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: default;
|
||||
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,
|
||||
&.pop.soft,
|
||||
&.success.soft,
|
||||
&.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);
|
||||
}
|
||||
.not-button {
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.reversedDirection {
|
||||
|
@ -65,7 +65,7 @@
|
||||
>
|
||||
{#if icon}
|
||||
<div class="textbox__icon">
|
||||
<Icon name={icon} />
|
||||
<Icon name={!disabled ? icon : 'locked'} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@ -152,8 +152,18 @@
|
||||
|
||||
.textbox__input-wrap {
|
||||
position: relative;
|
||||
&.disabled .textbox__icon {
|
||||
color: var(--clr-scale-ntrl-60);
|
||||
|
||||
&.disabled {
|
||||
/* background-color: var(--clr-bg-1); */
|
||||
& .textbox__icon {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +208,8 @@
|
||||
}
|
||||
|
||||
/* select */
|
||||
.textbox__input[type='select'] {
|
||||
.textbox__input[type='select']:not([disabled]),
|
||||
.textbox__input[type='select']:not([readonly]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,16 @@
|
||||
<span class="text-base-14 text-semibold">Telemetry</span>
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -35,6 +35,15 @@ export function 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> } {
|
||||
async function setAndPersist(value: T, set: (value: T) => void) {
|
||||
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 type { AnyFile, Commit, Hunk, RemoteCommit } from '../vbranches/types';
|
||||
import type { AnyCommit, AnyFile, Commit, Hunk, RemoteCommit } from '../vbranches/types';
|
||||
|
||||
export function nonDraggable() {
|
||||
return {
|
||||
@ -18,7 +18,8 @@ export class DraggableHunk {
|
||||
export class DraggableFile {
|
||||
constructor(
|
||||
public readonly branchId: string,
|
||||
private file: AnyFile,
|
||||
public file: AnyFile,
|
||||
public commit: AnyCommit | undefined,
|
||||
private selection: Readable<AnyFile[]> | undefined
|
||||
) {}
|
||||
|
||||
|
@ -22,7 +22,8 @@ export function showToast(toast: Toast) {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -17,6 +17,7 @@ export interface Settings {
|
||||
zoom: number;
|
||||
scrollbarVisabilityOnHover: boolean;
|
||||
tabSize: number;
|
||||
showHistoryView: boolean;
|
||||
}
|
||||
|
||||
const defaults: Settings = {
|
||||
@ -31,7 +32,8 @@ const defaults: Settings = {
|
||||
stashedBranchesHeight: 150,
|
||||
zoom: 1,
|
||||
scrollbarVisabilityOnHover: false,
|
||||
tabSize: 4
|
||||
tabSize: 4,
|
||||
showHistoryView: false
|
||||
};
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
await this.targetBranchService.setTarget(branch);
|
||||
return branch;
|
||||
// TODO: Reloading seems to trigger 4 invocations of `list_virtual_branches`
|
||||
} catch (err: any) {
|
||||
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 {
|
||||
await invoke<void>('amend_virtual_branch', {
|
||||
projectId: this.projectId,
|
||||
branchId,
|
||||
commitOid,
|
||||
ownership
|
||||
});
|
||||
} 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) {
|
||||
try {
|
||||
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[]) {
|
||||
return files
|
||||
@ -6,6 +6,15 @@ export function filesToOwnership(files: AnyFile[]) {
|
||||
.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.
|
||||
// TODO: refactor code for clarity, these types should not be needed
|
||||
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 {
|
||||
if (!commitId || commits === undefined) return 'Depends on a committed change';
|
||||
export function getLockText(hunkLocks: HunkLock | HunkLock[] | string, commits: Commit[]): string {
|
||||
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
|
||||
.map((id) => {
|
||||
const commit = commits.find((commit) => commit.id == id);
|
||||
const descriptions = locks
|
||||
.filter(unique)
|
||||
.map((lock) => {
|
||||
const commit = commits.find((c) => {
|
||||
return c.id == lock.commitId;
|
||||
});
|
||||
const shortCommitId = commit?.id.slice(0, 7);
|
||||
if (commit) {
|
||||
const shortTitle = commit.descriptionTitle?.slice(0, 35) + '...';
|
||||
@ -17,5 +21,13 @@ export function getLockText(commitId: string[] | string, commits: Commit[]): str
|
||||
}
|
||||
})
|
||||
.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 { splitMessage } from '$lib/utils/commitMessage';
|
||||
import { hashCode } from '$lib/utils/string';
|
||||
import { isDefined, notNull } from '$lib/utils/typeguards';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
|
||||
export type ChangeType =
|
||||
@ -21,8 +22,16 @@ export class Hunk {
|
||||
filePath!: string;
|
||||
hash?: string;
|
||||
locked!: boolean;
|
||||
lockedTo!: string | undefined;
|
||||
@Type(() => HunkLock)
|
||||
lockedTo!: HunkLock[];
|
||||
changeType!: ChangeType;
|
||||
new_start!: number;
|
||||
new_lines!: number;
|
||||
}
|
||||
|
||||
export class HunkLock {
|
||||
branchId!: string;
|
||||
commitId!: string;
|
||||
}
|
||||
|
||||
export type AnyFile = LocalFile | RemoteFile;
|
||||
@ -58,14 +67,15 @@ export class LocalFile {
|
||||
|
||||
get locked(): boolean {
|
||||
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;
|
||||
}
|
||||
|
||||
get lockedIds(): string[] {
|
||||
get lockedIds(): HunkLock[] {
|
||||
return this.hunks
|
||||
.map((hunk) => hunk.lockedTo)
|
||||
.filter((lockedTo): lockedTo is string => !!lockedTo);
|
||||
.flatMap((hunk) => hunk.lockedTo)
|
||||
.filter(notNull)
|
||||
.filter(isDefined);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,6 +220,8 @@ export const UNKNOWN_COMMITS = Symbol('UnknownCommits');
|
||||
export class RemoteHunk {
|
||||
diff!: string;
|
||||
hash?: string;
|
||||
new_start!: number;
|
||||
new_lines!: number;
|
||||
|
||||
get id(): string {
|
||||
return hashCode(this.diff);
|
||||
@ -250,7 +262,7 @@ export class RemoteFile {
|
||||
return this.hunks.map((h) => h.id);
|
||||
}
|
||||
|
||||
get lockedIds(): string[] {
|
||||
get lockedIds(): HunkLock[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,12 @@
|
||||
hotkeys.on('Backspace', (e) => {
|
||||
// This prevent backspace from navigating back
|
||||
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 { initPostHog } from '$lib/analytics/posthog';
|
||||
import { initSentry } from '$lib/analytics/sentry';
|
||||
import { initAnalyticsIfEnabled } from '$lib/analytics/analytics';
|
||||
import { AuthService } from '$lib/backend/auth';
|
||||
import { GitConfigService } from '$lib/backend/gitConfigService';
|
||||
import { HttpClient } from '$lib/backend/httpClient';
|
||||
import { ProjectService } from '$lib/backend/projects';
|
||||
import { PromptService } from '$lib/backend/prompt';
|
||||
import { UpdaterService } from '$lib/backend/updater';
|
||||
import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import lscache from 'lscache';
|
||||
@ -24,16 +22,7 @@ export const prerender = false;
|
||||
export const csr = true;
|
||||
|
||||
export async function load() {
|
||||
appErrorReportingEnabled()
|
||||
.onDisk()
|
||||
.then((enabled) => {
|
||||
if (enabled) initSentry();
|
||||
});
|
||||
appMetricsEnabled()
|
||||
.onDisk()
|
||||
.then((enabled) => {
|
||||
if (enabled) initPostHog();
|
||||
});
|
||||
initAnalyticsIfEnabled();
|
||||
|
||||
// TODO: Find a workaround to avoid this dynamic import
|
||||
// https://github.com/sveltejs/kit/issues/905
|
||||
|
@ -2,11 +2,14 @@
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { syncToCloud } from '$lib/backend/sync';
|
||||
import { BranchService } from '$lib/branches/service';
|
||||
import History from '$lib/components/History.svelte';
|
||||
import Navigation from '$lib/components/Navigation.svelte';
|
||||
import NoBaseBranch from '$lib/components/NoBaseBranch.svelte';
|
||||
import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte';
|
||||
import ProblemLoadingRepo from '$lib/components/ProblemLoadingRepo.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 { unsubscribe } from '$lib/utils/unsubscribe';
|
||||
import { BaseBranchService, NoDefaultTarget } from '$lib/vbranches/baseBranch';
|
||||
@ -33,6 +36,7 @@
|
||||
$: baseBranch = baseBranchService.base;
|
||||
$: baseError = baseBranchService.error;
|
||||
$: projectError = projectService.error;
|
||||
const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
|
||||
|
||||
$: setContext(VirtualBranchService, vbranchService);
|
||||
$: setContext(BranchController, branchController);
|
||||
@ -90,6 +94,9 @@
|
||||
<div class="view-wrap" role="group" on:dragover|preventDefault>
|
||||
<Navigation />
|
||||
<slot />
|
||||
{#if $userSettings.showHistoryView}
|
||||
<History {projectId} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Project, ProjectService } from '$lib/backend/projects';
|
||||
import BaseBranchSwitch from '$lib/components/BaseBranchSwitch.svelte';
|
||||
import CloudForm from '$lib/components/CloudForm.svelte';
|
||||
import DetailsForm from '$lib/components/DetailsForm.svelte';
|
||||
import KeysForm from '$lib/components/KeysForm.svelte';
|
||||
@ -8,6 +9,7 @@
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import Spacer from '$lib/components/Spacer.svelte';
|
||||
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
|
||||
import { featureBaseBranchSwitching } from '$lib/config/uiFeatureFlags';
|
||||
import { showError } from '$lib/notifications/toasts';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
@ -15,6 +17,7 @@
|
||||
import { from } from 'rxjs';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const baseBranchSwitching = featureBaseBranchSwitching();
|
||||
const projectService = getContext(ProjectService);
|
||||
const project = getContext(Project);
|
||||
const platformName = from(platform());
|
||||
@ -39,6 +42,9 @@
|
||||
</script>
|
||||
|
||||
<ContentWrapper title="Project settings">
|
||||
{#if $baseBranchSwitching}
|
||||
<BaseBranchSwitch />
|
||||
{/if}
|
||||
<CloudForm />
|
||||
<DetailsForm />
|
||||
{#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[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 {
|
||||
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 {
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
@ -82,7 +82,7 @@
|
||||
/* text Base Body Classes */
|
||||
.text-base-body-10 {
|
||||
font-family: var(--base-font-family);
|
||||
font-size: 625rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: var(--base-font-weight);
|
||||
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);
|
||||
}
|
||||
|
||||
const TEXT1: &str = include_str!("../fixtures/text1.txt");
|
||||
const TEXT2: &str = include_str!("../fixtures/text2.txt");
|
||||
const TEXT3: &str = include_str!("../fixtures/text3.txt");
|
||||
const CODE1: &str = include_str!("../fixtures/code1.txt");
|
||||
const CODE2: &str = include_str!("../fixtures/code2.txt");
|
||||
const CODE3: &str = include_str!("../fixtures/code3.txt");
|
||||
const CODE4: &str = include_str!("../fixtures/code4.txt");
|
||||
const LARGE1: &str = include_str!("../fixtures/large1.txt");
|
||||
const LARGE2: &str = include_str!("../fixtures/large2.txt");
|
||||
const TEXT1: &str = include_str!("fixtures/text1.txt");
|
||||
const TEXT2: &str = include_str!("fixtures/text2.txt");
|
||||
const TEXT3: &str = include_str!("fixtures/text3.txt");
|
||||
const CODE1: &str = include_str!("fixtures/code1.txt");
|
||||
const CODE2: &str = include_str!("fixtures/code2.txt");
|
||||
const CODE3: &str = include_str!("fixtures/code3.txt");
|
||||
const CODE4: &str = include_str!("fixtures/code4.txt");
|
||||
const LARGE1: &str = include_str!("fixtures/large1.txt");
|
||||
const LARGE2: &str = include_str!("fixtures/large2.txt");
|
||||
|
||||
macro_rules! real_test {
|
||||
($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]
|
||||
once_cell = "1.19"
|
||||
pretty_assertions = "1.4"
|
||||
tempfile = "3.10"
|
||||
gitbutler-testsupport.workspace = true
|
||||
gitbutler-git = { workspace = true, features = ["test-askpass-path" ]}
|
||||
|
||||
[dependencies]
|
||||
toml = "0.8.12"
|
||||
anyhow = "1.0.81"
|
||||
async-trait = "0.1.79"
|
||||
anyhow = "1.0.82"
|
||||
async-trait = "0.1.80"
|
||||
backtrace = { version = "0.3.71", optional = true }
|
||||
bstr = "1.9.1"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
diffy = "0.3.0"
|
||||
filetime = "0.2.23"
|
||||
fslock = "0.2.1"
|
||||
@ -33,7 +35,7 @@ r2d2_sqlite = "0.22.0"
|
||||
rand = "0.8.5"
|
||||
refinery = { version = "0.8", features = [ "rusqlite" ] }
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.12.2", features = ["json"] }
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
resolve-path = "0.1.0"
|
||||
rusqlite.workspace = true
|
||||
serde.workspace = true
|
||||
@ -41,8 +43,9 @@ serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
|
||||
sha2 = "0.10.8"
|
||||
similar = { version = "2.5.0", features = ["unicode"] }
|
||||
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"] }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
log = "^0.4"
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros" ] }
|
||||
@ -52,7 +55,6 @@ urlencoding = "2.1.3"
|
||||
uuid.workspace = true
|
||||
walkdir = "2.5.0"
|
||||
zip = "0.6.5"
|
||||
tempfile = "3.10"
|
||||
gitbutler-git.workspace = true
|
||||
|
||||
[features]
|
||||
|
@ -93,15 +93,12 @@ impl Proxy {
|
||||
|
||||
async fn proxy_author(&self, author: Author) -> Author {
|
||||
Author {
|
||||
gravatar_url: self
|
||||
.proxy(&author.gravatar_url)
|
||||
.await
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::error!(gravatar_url = %author.gravatar_url, ?error, "failed to proxy gravatar url");
|
||||
author.gravatar_url
|
||||
}),
|
||||
..author
|
||||
}
|
||||
gravatar_url: self.proxy(&author.gravatar_url).await.unwrap_or_else(|error| {
|
||||
tracing::error!(gravatar_url = %author.gravatar_url, ?error, "failed to proxy gravatar url");
|
||||
author.gravatar_url
|
||||
}),
|
||||
..author
|
||||
}
|
||||
}
|
||||
|
||||
async fn proxy_remote_commit(&self, commit: RemoteCommit) -> RemoteCommit {
|
||||
|
@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
time::SystemTime,
|
||||
};
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@ -50,29 +47,20 @@ impl Document {
|
||||
};
|
||||
|
||||
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 !value.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delta::Delta {
|
||||
operations,
|
||||
timestamp_ms: SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
}
|
||||
} else {
|
||||
delta::Delta {
|
||||
operations,
|
||||
timestamp_ms: SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
}
|
||||
let delta = delta::Delta {
|
||||
operations,
|
||||
timestamp_ms: crate::time::now_ms(),
|
||||
};
|
||||
|
||||
apply_deltas(&mut self.doc, &vec![delta.clone()])?;
|
||||
self.deltas.push(delta.clone());
|
||||
Ok(Some(delta))
|
||||
|
@ -1,8 +1,11 @@
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use bstr::BString;
|
||||
use gix::dir::walk::EmissionMode;
|
||||
use gix::tempfile::create_dir::Retries;
|
||||
use gix::tempfile::{AutoRemove, ContainingDirectory};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
// 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)
|
||||
.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,
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
path, time,
|
||||
path,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
@ -217,7 +217,8 @@ impl Repository {
|
||||
|
||||
// Push to the 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) => {
|
||||
tracing::warn!(project_id = %self.project.id, error = %error, "failed to push gb repo");
|
||||
RemoteError::Network
|
||||
@ -279,10 +280,7 @@ impl Repository {
|
||||
&self,
|
||||
project_repository: &project_repository::Repository,
|
||||
) -> Result<sessions::Session> {
|
||||
let now_ms = time::SystemTime::now()
|
||||
.duration_since(time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
let now_ms = crate::time::now_ms();
|
||||
|
||||
let meta = match project_repository.get_head() {
|
||||
Result::Ok(head) => sessions::Meta {
|
||||
@ -335,10 +333,7 @@ impl Repository {
|
||||
|
||||
let updated_session = sessions::Session {
|
||||
meta: sessions::Meta {
|
||||
last_timestamp_ms: time::SystemTime::now()
|
||||
.duration_since(time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
last_timestamp_ms: crate::time::now_ms(),
|
||||
..current_session.meta
|
||||
},
|
||||
..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 users = users::Controller::from_path(path);
|
||||
let home_dir = std::env::var_os("HOME").map(PathBuf::from);
|
||||
|
@ -9,7 +9,6 @@ use tracing::instrument;
|
||||
|
||||
use super::Repository;
|
||||
use crate::git;
|
||||
use crate::virtual_branches::BranchStatus;
|
||||
|
||||
pub type DiffByPathMap = HashMap<PathBuf, FileDiff>;
|
||||
|
||||
@ -53,6 +52,7 @@ pub struct GitHunk {
|
||||
#[serde(rename = "diff", serialize_with = "crate::serde::as_string_lossy")]
|
||||
pub diff_lines: BString,
|
||||
pub binary: bool,
|
||||
pub locked_to: Box<[HunkLock]>,
|
||||
pub change_type: ChangeType,
|
||||
}
|
||||
|
||||
@ -69,6 +69,7 @@ impl GitHunk {
|
||||
diff_lines: hex_id.into(),
|
||||
binary: true,
|
||||
change_type,
|
||||
locked_to: Box::new([]),
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +83,7 @@ impl GitHunk {
|
||||
diff_lines: Default::default(),
|
||||
binary: false,
|
||||
change_type: ChangeType::Modified,
|
||||
locked_to: Box::new([]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,6 +93,21 @@ impl GitHunk {
|
||||
pub fn contains(&self, line: u32) -> bool {
|
||||
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)]
|
||||
@ -298,6 +315,7 @@ fn hunks_by_filepath(repo: Option<&Repository>, diff: &git2::Diff) -> Result<Dif
|
||||
diff_lines: line.into_owned(),
|
||||
binary: false,
|
||||
change_type,
|
||||
locked_to: Box::new([]),
|
||||
}
|
||||
}
|
||||
LineOrHexHash::HexHashOfBinaryBlob(id) => {
|
||||
@ -404,12 +422,13 @@ pub fn reverse_hunk(hunk: &GitHunk) -> Option<GitHunk> {
|
||||
diff_lines: diff,
|
||||
binary: hunk.binary,
|
||||
change_type: hunk.change_type,
|
||||
locked_to: Box::new([]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(ST): turning this into an iterator will trigger a cascade of changes that
|
||||
// mean less unnecessary copies. It also leads to `virtual.rs` - 4k SLOC!
|
||||
pub fn diff_files_into_hunks(files: DiffByPathMap) -> BranchStatus {
|
||||
HashMap::from_iter(files.into_iter().map(|(path, file)| (path, file.hunks)))
|
||||
pub fn diff_files_into_hunks(
|
||||
files: DiffByPathMap,
|
||||
) -> impl Iterator<Item = (PathBuf, Vec<GitHunk>)> {
|
||||
files.into_iter().map(|(path, file)| (path, file.hunks))
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ pub enum Error {
|
||||
Hooks(#[from] git2_hooks::HooksError),
|
||||
#[error("http error: {0}")]
|
||||
Http(git2::Error),
|
||||
#[error("blame error: {0}")]
|
||||
Blame(git2::Error),
|
||||
#[error("checkout error: {0}")]
|
||||
Checkout(git2::Error),
|
||||
#[error(transparent)]
|
||||
|
@ -64,6 +64,8 @@ impl FromStr for Refname {
|
||||
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();
|
||||
|
||||
if let Some((remote, branch)) = value.split_once('/') {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{io::Write, path::Path, str};
|
||||
|
||||
use git2::Submodule;
|
||||
use git2::{BlameOptions, Submodule};
|
||||
use git2_hooks::HookResult;
|
||||
|
||||
use super::{
|
||||
@ -478,6 +478,24 @@ impl Repository {
|
||||
git2_hooks::hooks_post_commit(&self.0, Some(&["../.husky"]))?;
|
||||
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> {
|
||||
|
@ -1,4 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{storage::Storage, PrivateKey};
|
||||
|
||||
@ -12,7 +13,7 @@ impl Controller {
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -1,42 +1,40 @@
|
||||
use super::PrivateKey;
|
||||
use crate::storage;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// TODO(ST): get rid of this type, it's more trouble than it's worth.
|
||||
#[derive(Clone)]
|
||||
pub struct Storage {
|
||||
storage: storage::Storage,
|
||||
inner: storage::Storage,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IO error: {0}")]
|
||||
Storage(#[from] storage::Error),
|
||||
#[error(transparent)]
|
||||
Storage(#[from] std::io::Error),
|
||||
#[error("SSH key error: {0}")]
|
||||
SSHKey(#[from] ssh_key::Error),
|
||||
}
|
||||
|
||||
impl 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))
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Result<Option<PrivateKey>, Error> {
|
||||
self.storage
|
||||
.read("keys/ed25519")
|
||||
.map_err(Error::Storage)
|
||||
.and_then(|s| s.map(|s| s.parse().map_err(Error::SSHKey)).transpose())
|
||||
let key = self.inner.read("keys/ed25519")?;
|
||||
key.map(|s| s.parse().map_err(Into::into)).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> {
|
||||
self.storage
|
||||
.write("keys/ed25519", &key.to_string())
|
||||
.map_err(Error::Storage)?;
|
||||
self.storage
|
||||
.write("keys/ed25519.pub", &key.public_key().to_string())
|
||||
.map_err(Error::Storage)?;
|
||||
self.inner.write("keys/ed25519", &key.to_string())?;
|
||||
self.inner
|
||||
.write("keys/ed25519.pub", &key.public_key().to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -30,8 +30,10 @@ pub mod project_repository;
|
||||
pub mod projects;
|
||||
pub mod reader;
|
||||
pub mod sessions;
|
||||
pub mod snapshots;
|
||||
pub mod ssh;
|
||||
pub mod storage;
|
||||
pub mod time;
|
||||
pub mod types;
|
||||
pub mod users;
|
||||
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())
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let conflicts_path = repository.git_repository.path().join("conflicts");
|
||||
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 mut files = reader.lines().map_ok(PathBuf::from);
|
||||
if let Some(pathname) = path {
|
||||
// TODO(ST): This shouldn't work on UTF8 strings.
|
||||
let pathname = pathname.as_ref();
|
||||
|
||||
// check if pathname is one of the lines in conflicts_path file
|
||||
|
@ -61,7 +61,7 @@ impl Repository {
|
||||
// XXX(qix-): We will ultimately move away from an internal repository for a variety
|
||||
// XXX(qix-): of reasons, but for now, this is a simple, short-term solution that we
|
||||
// XXX(qix-): can clean up later on. We're aware this isn't ideal.
|
||||
if let Ok(config) = git_repository.config().as_mut(){
|
||||
if let Ok(config) = git_repository.config().as_mut() {
|
||||
let should_set = match config.get_bool("gitbutler.didSetPrune") {
|
||||
Ok(None | Some(false)) => true,
|
||||
Ok(Some(true)) => false,
|
||||
@ -76,7 +76,10 @@ impl Repository {
|
||||
};
|
||||
|
||||
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!(
|
||||
"failed to set gc.auto to false for repository at {}; cannot disable gc: {}",
|
||||
project.path.display(),
|
||||
@ -175,17 +178,11 @@ impl Repository {
|
||||
let branch = self.git_repository.find_branch(&target_branch_refname)?;
|
||||
let commit_id = branch.peel_to_commit()?.id();
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or(std::time::Duration::from_secs(0))
|
||||
.as_millis()
|
||||
.to_string();
|
||||
let branch_name = format!("test-push-{}", now);
|
||||
let now = crate::time::now_ms();
|
||||
let branch_name = format!("test-push-{now}");
|
||||
|
||||
let refname = git::RemoteRefname::from_str(&format!(
|
||||
"refs/remotes/{}/{}",
|
||||
remote_name, branch_name,
|
||||
))?;
|
||||
let refname =
|
||||
git::RemoteRefname::from_str(&format!("refs/remotes/{remote_name}/{branch_name}",))?;
|
||||
|
||||
match self.push(
|
||||
&commit_id,
|
||||
@ -623,6 +620,8 @@ pub enum RemoteError {
|
||||
Network,
|
||||
#[error("authentication failed")]
|
||||
Auth,
|
||||
#[error("Git failed")]
|
||||
Git(#[from] git::Error),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
@ -638,6 +637,9 @@ impl ErrorWithContext for RemoteError {
|
||||
Code::ProjectGitAuth,
|
||||
"Project remote authentication error",
|
||||
),
|
||||
RemoteError::Git(_) => {
|
||||
error::Context::new_static(Code::ProjectGitRemote, "Git command failed")
|
||||
}
|
||||
RemoteError::Other(error) => {
|
||||
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