mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-24 18:12:48 +03:00
Merged origin/master into sidebar-branches
This commit is contained in:
commit
2e3e733ac5
3
.github/actions/init-env-node/action.yaml
vendored
3
.github/actions/init-env-node/action.yaml
vendored
@ -36,4 +36,5 @@ runs:
|
||||
|
||||
- name: Build UI
|
||||
shell: bash
|
||||
run: cd packages/ui && pnpm package
|
||||
run: pnpm exec turbo run package # build UI package
|
||||
|
||||
|
10
.github/actions/init-env-rust/action.yaml
vendored
10
.github/actions/init-env-rust/action.yaml
vendored
@ -3,6 +3,16 @@ description: prepare runner for rust related tasks
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Nightly
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
mv rust-toolchain.toml.windows rust-toolchain.toml
|
||||
- name: Setup Stable
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
mv rust-toolchain.toml.stable rust-toolchain.toml
|
||||
- name: Check versions
|
||||
shell: bash
|
||||
run: |
|
||||
|
10
.github/pr-labeler.yml
vendored
10
.github/pr-labeler.yml
vendored
@ -4,10 +4,14 @@ rust:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: crates/**/*
|
||||
|
||||
app:
|
||||
"@gitbutler/desktop":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: app/**/*
|
||||
- any-glob-to-any-file: apps/desktop/**/*
|
||||
|
||||
ui:
|
||||
"@gitbutler/web":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: app/web/**/*
|
||||
|
||||
"@gitbutler/ui":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: packages/ui/**/*
|
||||
|
2
.github/workflows/push.yaml
vendored
2
.github/workflows/push.yaml
vendored
@ -32,7 +32,6 @@ jobs:
|
||||
- *workflows
|
||||
- 'Cargo.lock'
|
||||
- 'Cargo.toml'
|
||||
- 'rust-toolchain.toml'
|
||||
rust: &any-rust
|
||||
- *rust
|
||||
- 'crates/**'
|
||||
@ -111,7 +110,6 @@ jobs:
|
||||
- [devtools]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/init-env-rust
|
||||
- uses: ./.github/actions/check-crate
|
||||
with:
|
||||
features: ${{ toJson(matrix.features) }}
|
||||
|
12
.github/workflows/test-e2e.yml
vendored
12
.github/workflows/test-e2e.yml
vendored
@ -14,17 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: |
|
||||
pnpm-lock.yaml
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Build @gitbutler/ui
|
||||
run: cd packages/ui && pnpm package
|
||||
- uses: ./.github/actions/init-env-node
|
||||
- name: Get installed Playwright version
|
||||
id: playwright-version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./apps/desktop/package.json').devDependencies['@playwright/test'].substring(1))")" >> $GITHUB_ENV
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,5 +1,8 @@
|
||||
# will have compiled rust files and executables
|
||||
target/
|
||||
generated-archives/
|
||||
generated-do-not-edit/
|
||||
|
||||
|
||||
# editors
|
||||
.idea
|
||||
@ -40,3 +43,6 @@ playwright-report
|
||||
# storybook
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -29,5 +29,8 @@
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[mdx]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
198
Cargo.lock
generated
198
Cargo.lock
generated
@ -847,6 +847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -861,6 +862,18 @@ dependencies = [
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.1"
|
||||
@ -1033,6 +1046,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
@ -1396,17 +1424,6 @@ 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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
@ -1417,16 +1434,6 @@ 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"
|
||||
@ -1600,6 +1607,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@ -2011,6 +2024,7 @@ dependencies = [
|
||||
"gitbutler-fs",
|
||||
"gitbutler-git",
|
||||
"gitbutler-id",
|
||||
"gitbutler-operating-modes",
|
||||
"gitbutler-oplog",
|
||||
"gitbutler-project",
|
||||
"gitbutler-reference",
|
||||
@ -2045,9 +2059,14 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dirs-next",
|
||||
"gitbutler-branch",
|
||||
"gitbutler-branch-actions",
|
||||
"gitbutler-diff",
|
||||
"gitbutler-oplog",
|
||||
"gitbutler-project",
|
||||
"pager",
|
||||
"gitbutler-reference",
|
||||
"gix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2170,6 +2189,16 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler-operating-modes"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"git2",
|
||||
"gitbutler-command-context",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler-oplog"
|
||||
version = "0.0.0"
|
||||
@ -2247,6 +2276,7 @@ dependencies = [
|
||||
"gitbutler-time",
|
||||
"gitbutler-url",
|
||||
"gitbutler-user",
|
||||
"gix",
|
||||
"log",
|
||||
"resolve-path",
|
||||
"serde",
|
||||
@ -2342,6 +2372,7 @@ dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"open 5.3.0",
|
||||
"parking_lot 0.12.3",
|
||||
"pretty_assertions",
|
||||
"reqwest 0.12.5",
|
||||
"serde",
|
||||
@ -2376,8 +2407,10 @@ dependencies = [
|
||||
"gitbutler-storage",
|
||||
"gitbutler-url",
|
||||
"gitbutler-user",
|
||||
"gix-testtools",
|
||||
"keyring",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
@ -2420,6 +2453,7 @@ dependencies = [
|
||||
"gitbutler-command-context",
|
||||
"gitbutler-error",
|
||||
"gitbutler-notify-debouncer",
|
||||
"gitbutler-operating-modes",
|
||||
"gitbutler-oplog",
|
||||
"gitbutler-project",
|
||||
"gitbutler-reference",
|
||||
@ -2448,7 +2482,7 @@ dependencies = [
|
||||
"gix-date",
|
||||
"gix-diff",
|
||||
"gix-dir",
|
||||
"gix-discover",
|
||||
"gix-discover 0.33.0",
|
||||
"gix-features",
|
||||
"gix-filter",
|
||||
"gix-fs",
|
||||
@ -2466,7 +2500,7 @@ dependencies = [
|
||||
"gix-path",
|
||||
"gix-pathspec",
|
||||
"gix-prompt",
|
||||
"gix-ref",
|
||||
"gix-ref 0.45.0",
|
||||
"gix-refspec",
|
||||
"gix-revision",
|
||||
"gix-revwalk",
|
||||
@ -2570,7 +2604,7 @@ dependencies = [
|
||||
"gix-features",
|
||||
"gix-glob",
|
||||
"gix-path",
|
||||
"gix-ref",
|
||||
"gix-ref 0.45.0",
|
||||
"gix-sec",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@ -2641,7 +2675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c975679aa00dd2d757bfd3ddb232e8a188c0094c3306400575a0813858b1365"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-discover",
|
||||
"gix-discover 0.33.0",
|
||||
"gix-fs",
|
||||
"gix-ignore",
|
||||
"gix-index",
|
||||
@ -2654,6 +2688,22 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-discover"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"dunce",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-path",
|
||||
"gix-ref 0.44.1",
|
||||
"gix-sec",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-discover"
|
||||
version = "0.33.0"
|
||||
@ -2665,7 +2715,7 @@ dependencies = [
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-path",
|
||||
"gix-ref",
|
||||
"gix-ref 0.45.0",
|
||||
"gix-sec",
|
||||
"thiserror",
|
||||
]
|
||||
@ -2956,6 +3006,28 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-ref"
|
||||
version = "0.44.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3394a2997e5bc6b22ebc1e1a87b41eeefbcfcff3dbfa7c4bd73cb0ac8f1f3e2e"
|
||||
dependencies = [
|
||||
"gix-actor",
|
||||
"gix-date",
|
||||
"gix-features",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-lock",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-tempfile",
|
||||
"gix-utils",
|
||||
"gix-validate",
|
||||
"memmap2",
|
||||
"thiserror",
|
||||
"winnow 0.6.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-ref"
|
||||
version = "0.45.0"
|
||||
@ -3057,9 +3129,37 @@ dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"signal-hook",
|
||||
"signal-hook-registry",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-testtools"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33fd7cd1816d78db635003c9e3fc667a1671689c678de2b92ce7c71ed2d58686"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crc",
|
||||
"fastrand 2.1.0",
|
||||
"fs_extra",
|
||||
"gix-discover 0.32.0",
|
||||
"gix-fs",
|
||||
"gix-ignore",
|
||||
"gix-index",
|
||||
"gix-lock",
|
||||
"gix-tempfile",
|
||||
"gix-worktree",
|
||||
"io-close",
|
||||
"is_ci",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"winnow 0.6.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-trace"
|
||||
version = "0.1.9"
|
||||
@ -3782,6 +3882,16 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-close"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
@ -3818,6 +3928,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_ci"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
@ -4579,9 +4695,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.66"
|
||||
version = "0.10.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
@ -4664,16 +4780,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[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"
|
||||
@ -5666,7 +5772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno 0.3.9",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys 0.3.8",
|
||||
@ -5680,7 +5786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno 0.3.9",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"windows-sys 0.52.0",
|
||||
@ -6072,6 +6178,16 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
|
@ -26,15 +26,15 @@ members = [
|
||||
"crates/gitbutler-time",
|
||||
"crates/gitbutler-commit",
|
||||
"crates/gitbutler-tagged-string",
|
||||
"crates/gitbutler-url",
|
||||
"crates/gitbutler-url",
|
||||
"crates/gitbutler-diff",
|
||||
"crates/gitbutler-operating-modes",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes.
|
||||
gix = { version = "0.64", default-features = false, features = [
|
||||
] }
|
||||
gix = { version = "0.64", default-features = false, features = [] }
|
||||
git2 = { version = "0.18.3", features = [
|
||||
"vendored-openssl",
|
||||
"vendored-libgit2",
|
||||
@ -47,6 +47,7 @@ keyring = "2.3.3"
|
||||
anyhow = "1.0.86"
|
||||
fslock = "0.2.1"
|
||||
parking_lot = "0.12.3"
|
||||
futures = "0.3.30"
|
||||
|
||||
gitbutler-id = { path = "crates/gitbutler-id" }
|
||||
gitbutler-git = { path = "crates/gitbutler-git" }
|
||||
@ -74,6 +75,7 @@ gitbutler-commit = { path = "crates/gitbutler-commit" }
|
||||
gitbutler-tagged-string = { path = "crates/gitbutler-tagged-string" }
|
||||
gitbutler-url = { path = "crates/gitbutler-url" }
|
||||
gitbutler-diff = { path = "crates/gitbutler-diff" }
|
||||
gitbutler-operating-modes = { path = "crates/gitbutler-operating-modes" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
|
@ -18,9 +18,9 @@ you right. Let's get started.
|
||||
- [Tokio](#tokio)
|
||||
- [Building](#building)
|
||||
- [Building on Windows](#building-on-windows)
|
||||
- [File permissions](#file-permissions)
|
||||
- [Perl](#perl)
|
||||
- [Crosscompilation](#crosscompilation)
|
||||
- [File permissions](#file-permissions)
|
||||
- [Perl](#perl)
|
||||
- [Crosscompilation](#crosscompilation)
|
||||
- [Design](#design)
|
||||
- [Contributing](#contributing)
|
||||
- [Some Other Random Notes](#some-other-random-notes)
|
||||
@ -86,13 +86,13 @@ $ cargo build
|
||||
Now you should be able to run the app in development mode:
|
||||
|
||||
```bash
|
||||
$ pnpm tauri dev
|
||||
$ pnpm dev:desktop
|
||||
```
|
||||
|
||||
By default it will not print debug logs to console. If you want debug logs, set `LOG_LEVEL` environment variable:
|
||||
|
||||
```bash
|
||||
$ LOG_LEVEL=debug pnpm tauri dev
|
||||
$ LOG_LEVEL=debug pnpm dev:desktop
|
||||
```
|
||||
|
||||
### Lint & format
|
||||
@ -162,7 +162,22 @@ This will make an asset similar to our nightly build.
|
||||
|
||||
Building on Windows is a bit of a tricky process. Here are some helpful tips.
|
||||
|
||||
### File permissions
|
||||
#### Nightly Compiler
|
||||
|
||||
As a few crates require nightly features on Windows, it's easiest to set an override
|
||||
to automatically use a nightly compiler.
|
||||
|
||||
```shell
|
||||
rustup override add nightly-2024-07-01
|
||||
```
|
||||
|
||||
If a stable nightly isn't desired or necessary, the latest nightly compiler can also be used:
|
||||
|
||||
```shell
|
||||
rustup override add nightly
|
||||
```
|
||||
|
||||
#### File permissions
|
||||
|
||||
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
|
||||
@ -184,7 +199,7 @@ npm config set prefix $env:APPDATA\npm
|
||||
|
||||
Afterwards, add this folder to your PATH.
|
||||
|
||||
### Perl
|
||||
#### Perl
|
||||
|
||||
A Perl interpreter is required to be installed in order to configure the `openssl-sys`
|
||||
crate. We've used [Strawberry Perl](https://strawberryperl.com/) without issue.
|
||||
@ -196,7 +211,7 @@ Note that it might appear that the build has hung or frozen on the `openssl-sys`
|
||||
It's not, it's just that Cargo can't report the status of a C/C++ build happening
|
||||
under the hood, and openssl is _large_. It'll take a while to compile.
|
||||
|
||||
### Crosscompilation
|
||||
#### Crosscompilation
|
||||
|
||||
This paragraph is about crosscompilation to x86_64-MSVC from ARM Windows,
|
||||
a configuration typical for people with Apple Silicon and Parallels VMs,
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@gitbutler/app",
|
||||
"name": "@gitbutler/desktop",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { convertRemoteToWebUrl } from '$lib/utils/url';
|
||||
import { Commit } from '$lib/vbranches/types';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
@ -27,71 +26,7 @@ export class BaseBranch {
|
||||
return this.lastFetchedMs ? new Date(this.lastFetchedMs) : undefined;
|
||||
}
|
||||
|
||||
get pushRepoBaseUrl(): string {
|
||||
return convertRemoteToWebUrl(this.pushRemoteUrl);
|
||||
}
|
||||
|
||||
get repoBaseUrl(): string {
|
||||
return convertRemoteToWebUrl(this.remoteUrl);
|
||||
}
|
||||
|
||||
commitUrl(commitId: string): string | undefined {
|
||||
// Different Git providers use different paths for the commit url:
|
||||
if (this.isBitBucket) {
|
||||
return `${this.pushRepoBaseUrl}/commits/${commitId}`;
|
||||
}
|
||||
if (this.isGitlab) {
|
||||
return `${this.pushRepoBaseUrl}/-/commit/${commitId}`;
|
||||
}
|
||||
return `${this.pushRepoBaseUrl}/commit/${commitId}`;
|
||||
}
|
||||
|
||||
get shortName() {
|
||||
return this.branchName.split('/').slice(-1)[0];
|
||||
}
|
||||
|
||||
branchUrl(upstreamBranchName: string | undefined) {
|
||||
if (!upstreamBranchName) return undefined;
|
||||
const baseBranchName = this.branchName.split('/')[1];
|
||||
const branchName = upstreamBranchName.split('/').slice(3).join('/');
|
||||
|
||||
if (this.pushRemoteName) {
|
||||
if (this.isGitHub) {
|
||||
// master...schacon:docs:Virtual-branch
|
||||
const pushUsername = this.extractUsernameFromGitHubUrl(this.pushRemoteUrl);
|
||||
const pushRepoName = this.extractRepoNameFromGitHubUrl(this.pushRemoteUrl);
|
||||
return `${this.repoBaseUrl}/compare/${baseBranchName}...${pushUsername}:${pushRepoName}:${branchName}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isBitBucket) {
|
||||
return `${this.repoBaseUrl}/branch/${branchName}?dest=${baseBranchName}`;
|
||||
}
|
||||
// The following branch path is good for at least Gitlab and Github:
|
||||
return `${this.repoBaseUrl}/compare/${baseBranchName}...${branchName}`;
|
||||
}
|
||||
|
||||
private extractUsernameFromGitHubUrl(url: string): string | null {
|
||||
const regex = /github\.com[/:]([a-zA-Z0-9_-]+)\/.*/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
private extractRepoNameFromGitHubUrl(url: string): string | null {
|
||||
const regex = /github\.com[/:]([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_-]+)/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[2] : null;
|
||||
}
|
||||
|
||||
private get isGitHub(): boolean {
|
||||
return this.repoBaseUrl.includes('github.com');
|
||||
}
|
||||
|
||||
private get isBitBucket(): boolean {
|
||||
return this.repoBaseUrl.includes('bitbucket.org');
|
||||
}
|
||||
|
||||
private get isGitlab(): boolean {
|
||||
return this.repoBaseUrl.includes('gitlab.com');
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import { getNameNormalizationServiceContext } from '$lib/branches/nameNormalizationService';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import { getContextStore } from '$lib/utils/context';
|
||||
import { openExternalUrl } from '$lib/utils/url';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
|
||||
export let isUnapplied = false;
|
||||
export let hasIntegratedCommits = false;
|
||||
export let isLaneCollapsed: boolean;
|
||||
export let remoteExists: boolean;
|
||||
const {
|
||||
isUnapplied = false,
|
||||
hasIntegratedCommits = false,
|
||||
isLaneCollapsed,
|
||||
remoteExists
|
||||
}: {
|
||||
isUnapplied?: boolean;
|
||||
hasIntegratedCommits?: boolean;
|
||||
isLaneCollapsed: boolean;
|
||||
remoteExists: boolean;
|
||||
} = $props();
|
||||
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
const branch = getContextStore(VirtualBranch);
|
||||
const upstreamName = $derived($branch.upstreamName);
|
||||
const gitHost = getGitHost();
|
||||
const gitHostBranch = $derived(upstreamName ? $gitHost?.branch(upstreamName) : undefined);
|
||||
|
||||
const nameNormalizationService = getNameNormalizationServiceContext();
|
||||
|
||||
let normalizedBranchName: string;
|
||||
let normalizedBranchName: string | undefined = $state();
|
||||
|
||||
$: if ($branch.displayName) {
|
||||
$effect(() => {
|
||||
nameNormalizationService
|
||||
.normalize($branch.displayName)
|
||||
.then((name) => {
|
||||
@ -27,7 +36,7 @@
|
||||
.catch((e) => {
|
||||
console.error('Failed to normalize branch name', e);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !remoteExists}
|
||||
@ -82,7 +91,7 @@
|
||||
outline
|
||||
shrinkable
|
||||
on:click={(e) => {
|
||||
const url = $baseBranch?.branchUrl($branch.upstream?.name);
|
||||
const url = gitHostBranch?.url;
|
||||
if (url) openExternalUrl(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import BranchFooter from './BranchFooter.svelte';
|
||||
import BranchHeader from './BranchHeader.svelte';
|
||||
import EmptyStatePlaceholder from '../components/EmptyStatePlaceholder.svelte';
|
||||
import PullRequestCard from '../pr/PullRequestCard.svelte';
|
||||
@ -180,10 +179,7 @@
|
||||
</Dropzones>
|
||||
{/if}
|
||||
|
||||
<div class="card-commits">
|
||||
<CommitList isUnapplied={false} />
|
||||
<BranchFooter />
|
||||
</div>
|
||||
<CommitList isUnapplied={false} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
@ -1,143 +0,0 @@
|
||||
<script lang="ts">
|
||||
import PassphraseBox from './PassphraseBox.svelte';
|
||||
import PushButton, { BranchAction } from '../components/PushButton.svelte';
|
||||
import emptyStateImg from '$lib/assets/empty-state/commits-up-to-date.svg?raw';
|
||||
import { PromptService } from '$lib/backend/prompt';
|
||||
import { getGitHostChecksMonitor } from '$lib/gitHost/interface/gitHostChecksMonitor';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import { getGitHostPrMonitor } from '$lib/gitHost/interface/gitHostPrMonitor';
|
||||
import { project } from '$lib/testing/fixtures';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { intersectionObserver } from '$lib/utils/intersectionObserver';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import {
|
||||
getLocalCommits,
|
||||
getLocalAndRemoteCommits,
|
||||
getRemoteCommits
|
||||
} from '$lib/vbranches/contexts';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const promptService = getContext(PromptService);
|
||||
const branch = getContextStore(VirtualBranch);
|
||||
|
||||
const listingService = getGitHostListingService();
|
||||
const prMonitor = getGitHostPrMonitor();
|
||||
const checksMonitor = getGitHostChecksMonitor();
|
||||
|
||||
const [prompt, promptError] = promptService.reactToPrompt({
|
||||
branchId: $branch.id,
|
||||
timeoutMs: 30000
|
||||
});
|
||||
|
||||
const localCommits = getLocalCommits();
|
||||
const localAndRemoteCommits = getLocalAndRemoteCommits();
|
||||
const remoteCommits = getRemoteCommits();
|
||||
|
||||
let isLoading: boolean;
|
||||
let isInViewport = false;
|
||||
|
||||
$: canBePushed = $localCommits.length !== 0 || $remoteCommits.length !== 0;
|
||||
$: hasRemoteCommits = $remoteCommits.length > 0;
|
||||
$: hasCommits =
|
||||
$localCommits.length > 0 || $localAndRemoteCommits.length > 0 || $remoteCommits.length > 0;
|
||||
</script>
|
||||
|
||||
{#if hasCommits}
|
||||
<div
|
||||
class="actions"
|
||||
class:sticky={canBePushed}
|
||||
class:not-in-viewport={!isInViewport}
|
||||
use:intersectionObserver={{
|
||||
callback: (entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
isInViewport = true;
|
||||
} else {
|
||||
isInViewport = false;
|
||||
}
|
||||
},
|
||||
options: {
|
||||
root: null,
|
||||
rootMargin: '-1px',
|
||||
threshold: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if canBePushed}
|
||||
{#if $prompt}
|
||||
<PassphraseBox prompt={$prompt} error={$promptError} />
|
||||
{/if}
|
||||
<PushButton
|
||||
wide
|
||||
projectId={project.id}
|
||||
requiresForce={$branch.requiresForce}
|
||||
integrate={hasRemoteCommits}
|
||||
{isLoading}
|
||||
on:trigger={async (e) => {
|
||||
isLoading = true;
|
||||
try {
|
||||
if (e.detail.action === BranchAction.Push) {
|
||||
await branchController.pushBranch($branch.id, $branch.requiresForce);
|
||||
$listingService?.refresh();
|
||||
$prMonitor?.refresh();
|
||||
$checksMonitor?.update();
|
||||
} else if (e.detail.action === BranchAction.Integrate) {
|
||||
await branchController.mergeUpstream($branch.id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<span class="text-base-body-12 empty-state__text"
|
||||
>Your branch is up to date with the remote.</span
|
||||
>
|
||||
|
||||
<i class="empty-state__image">
|
||||
{@html emptyStateImg}
|
||||
</i>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.actions {
|
||||
background: var(--clr-bg-1);
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
}
|
||||
|
||||
/* EMPTY STATE */
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.empty-state__image {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.empty-state__text {
|
||||
color: var(--clr-text-3);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
.sticky {
|
||||
z-index: var(--z-lifted);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.not-in-viewport {
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import BranchLabel from './BranchLabel.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
@ -8,20 +9,21 @@
|
||||
import { openExternalUrl } from '$lib/utils/url';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { tooltip } from '@gitbutler/ui/utils/tooltip';
|
||||
import type { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
import type { Branch } from '$lib/vbranches/types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let localBranch: Branch | undefined;
|
||||
export let remoteBranch: Branch | undefined;
|
||||
export let base: BaseBranch | undefined | null;
|
||||
export let pr: PullRequest | undefined;
|
||||
|
||||
$: branch = remoteBranch || localBranch!;
|
||||
$: upstream = branch.upstream;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const project = getContext(Project);
|
||||
const gitHost = getGitHost();
|
||||
const gitHostBranch = upstream ? $gitHost?.branch(upstream) : undefined;
|
||||
|
||||
let isApplying = false;
|
||||
</script>
|
||||
@ -46,7 +48,7 @@
|
||||
outline
|
||||
shrinkable
|
||||
on:click={(e) => {
|
||||
const url = base?.branchUrl(branch.name);
|
||||
const url = gitHostBranch?.url;
|
||||
if (url) openExternalUrl(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -1,20 +1,44 @@
|
||||
import { invoke } from '$lib/backend/ipc';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
|
||||
export class BranchListingService {
|
||||
constructor(private projectId: string) {}
|
||||
async list() {
|
||||
async list(filter: BranchListingFilter | undefined = undefined) {
|
||||
try {
|
||||
const branches = plainToInstance(
|
||||
BranchListing,
|
||||
await invoke<any[]>('list_branches', { projectId: this.projectId })
|
||||
await invoke<any[]>('list_branches', { projectId: this.projectId, filter })
|
||||
);
|
||||
console.log(branches);
|
||||
return branches;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
async get_branch_listing_details(branchNames: string[]) {
|
||||
try {
|
||||
const branches = plainToInstance(
|
||||
BranchListingDetails,
|
||||
await invoke<any[]>('get_branch_listing_details', {
|
||||
projectId: this.projectId,
|
||||
branchNames
|
||||
})
|
||||
);
|
||||
return branches;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A filter that can be applied to the branch listing
|
||||
export class BranchListingFilter {
|
||||
/// If the value is true, the listing will only include branches that have the same author as the current user.
|
||||
/// If the value is false, the listing will include only branches that are not created by the user.
|
||||
ownBranches?: boolean;
|
||||
/// If the value is true, the listing will only include branches that are applied in the workspace.
|
||||
/// If the value is false, the listing will only include branches that are not applied in the workspace.
|
||||
applied?: boolean;
|
||||
}
|
||||
|
||||
/// Represents a branch that exists for the repository
|
||||
@ -30,23 +54,6 @@ export class BranchListing {
|
||||
remotes!: string[];
|
||||
/// The branch may or may not have a virtual branch associated with it
|
||||
virtualBranch?: VirtualBranchReference | undefined;
|
||||
/// The number of lines added within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
/// If this branch has a virutal branch, lines_added does NOT include the uncommitted lines.
|
||||
linesAdded!: number;
|
||||
/// The number of lines removed within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
/// If this branch has a virutal branch, lines_removed does NOT include the uncommitted lines.
|
||||
linesRemoved!: number;
|
||||
/// The number of files that were modified within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number files modified,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
numberOfFiles!: number;
|
||||
/// The number of commits associated with a branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of commits,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
@ -54,6 +61,7 @@ export class BranchListing {
|
||||
numberOfCommits!: number;
|
||||
/// Timestamp in milliseconds since the branch was last updated.
|
||||
/// This includes any commits, uncommited changes or even updates to the branch metadata (e.g. renaming).
|
||||
@Transform((obj) => new Date(obj.value))
|
||||
updatedAt!: number;
|
||||
/// A list of authors that have contributes commits to this branch.
|
||||
/// In the case of multiple remote tracking branches, it takes the full list of unique authors.
|
||||
@ -80,3 +88,26 @@ export class Author {
|
||||
/// The email of the author as configured in the git config
|
||||
email?: string | undefined;
|
||||
}
|
||||
|
||||
/// Represents a fat struct with all the data associated with a branch
|
||||
export class BranchListingDetails {
|
||||
/// The name of the branch (e.g. `main`, `feature/branch`), excluding the remote name
|
||||
name!: string;
|
||||
/// The number of lines added within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple).
|
||||
/// If this branch has a virutal branch, lines_added does NOT include the uncommitted lines.
|
||||
lines_added!: number;
|
||||
/// The number of lines removed within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
/// If this branch has a virutal branch, lines_removed does NOT include the uncommitted lines.
|
||||
lines_removed!: number;
|
||||
/// The number of files that were modified within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number files modified,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
number_of_files!: number;
|
||||
}
|
||||
|
81
apps/desktop/src/lib/commit/CommitAction.svelte
Normal file
81
apps/desktop/src/lib/commit/CommitAction.svelte
Normal file
@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { intersectionObserver } from '$lib/utils/intersectionObserver';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
bottomBorder?: boolean;
|
||||
lines: Snippet;
|
||||
action: Snippet;
|
||||
}
|
||||
|
||||
const { bottomBorder = true, lines, action }: Props = $props();
|
||||
|
||||
let isNotInViewport = $state(false);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="action-row sticky"
|
||||
class:not-in-viewport={!isNotInViewport}
|
||||
class:sticky-z-index={!isNotInViewport}
|
||||
class:bottom-border={bottomBorder}
|
||||
use:intersectionObserver={{
|
||||
callback: (entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
isNotInViewport = false;
|
||||
} else {
|
||||
isNotInViewport = true;
|
||||
}
|
||||
},
|
||||
options: {
|
||||
root: null,
|
||||
rootMargin: `-100% 0 0 0`,
|
||||
threshold: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{@render lines()}
|
||||
</div>
|
||||
<div class="action">
|
||||
{@render action()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.action-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
background-color: var(--clr-bg-2);
|
||||
overflow: hidden;
|
||||
|
||||
transition: border-top var(--transition-fast);
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
padding-right: 14px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
.bottom-border {
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.sticky-z-index {
|
||||
z-index: var(--z-lifted);
|
||||
}
|
||||
|
||||
.not-in-viewport {
|
||||
box-shadow: 0 0 0 1px var(--clr-border-2);
|
||||
}
|
||||
</style>
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import CommitAction from './CommitAction.svelte';
|
||||
import CommitCard from './CommitCard.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
@ -10,6 +11,11 @@
|
||||
} from '$lib/dragging/reorderDropzoneManager';
|
||||
import Dropzone from '$lib/dropzone/Dropzone.svelte';
|
||||
import LineOverlay from '$lib/dropzone/LineOverlay.svelte';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { getGitHostChecksMonitor } from '$lib/gitHost/interface/gitHostChecksMonitor';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import { getGitHostPrMonitor } from '$lib/gitHost/interface/gitHostPrMonitor';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import { getContextStore } from '$lib/utils/context';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
@ -35,13 +41,27 @@
|
||||
const project = getContext(Project);
|
||||
const branchController = getContext(BranchController);
|
||||
const lineManagerFactory = getContext(LineManagerFactory);
|
||||
//
|
||||
const listingService = getGitHostListingService();
|
||||
const prMonitor = getGitHostPrMonitor();
|
||||
const checksMonitor = getGitHostChecksMonitor();
|
||||
|
||||
const reorderDropzoneManagerFactory = getContext(ReorderDropzoneManagerFactory);
|
||||
const gitHost = getGitHost();
|
||||
|
||||
$: mappedRemoteCommits =
|
||||
$remoteCommits.length > 0
|
||||
? [...$remoteCommits.map(transformAnyCommit), { id: 'remote-spacer' }]
|
||||
: [];
|
||||
$: mappedLocalCommits =
|
||||
$localCommits.length > 0
|
||||
? [...$localCommits.map(transformAnyCommit), { id: 'local-spacer' }]
|
||||
: [];
|
||||
|
||||
$: lineManager = lineManagerFactory.build(
|
||||
{
|
||||
remoteCommits: $remoteCommits.map(transformAnyCommit),
|
||||
localCommits: $localCommits.map(transformAnyCommit),
|
||||
remoteCommits: mappedRemoteCommits,
|
||||
localCommits: mappedLocalCommits,
|
||||
localAndRemoteCommits: $localAndRemoteCommits.map(transformAnyCommit),
|
||||
integratedCommits: $integratedCommits.map(transformAnyCommit)
|
||||
},
|
||||
@ -69,6 +89,9 @@
|
||||
$: upstreamForkPoint = $branch.upstreamData?.forkPoint;
|
||||
$: isRebased = !!forkPoint && !!upstreamForkPoint && forkPoint !== upstreamForkPoint;
|
||||
|
||||
$: isPushingCommits = false;
|
||||
$: isIntegratingCommits = false;
|
||||
|
||||
let baseIsUnfolded = false;
|
||||
|
||||
function insertBlankCommit(commitId: string, location: 'above' | 'below' = 'below') {
|
||||
@ -106,110 +129,177 @@
|
||||
{#if hasCommits || hasRemoteCommits}
|
||||
<div class="commits">
|
||||
<!-- UPSTREAM COMMITS -->
|
||||
|
||||
{#if $remoteCommits.length > 0}
|
||||
{#each $remoteCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
type="remote"
|
||||
branch={$branch}
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
first={idx === 0}
|
||||
last={idx === $remoteCommits.length - 1}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
<!-- To make the sticky position work, commits should be wrapped in a div -->
|
||||
<div class="commits-group">
|
||||
{#each $remoteCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
type="remote"
|
||||
branch={$branch}
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
first={idx === 0}
|
||||
last={idx === $remoteCommits.length - 1}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
|
||||
<CommitAction>
|
||||
{#snippet lines()}
|
||||
<LineGroup lineGroup={lineManager.get('remote-spacer')} topHeightPx={0} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
{#snippet action()}
|
||||
<Button
|
||||
style="warning"
|
||||
kind="solid"
|
||||
loading={isIntegratingCommits}
|
||||
on:click={async () => {
|
||||
isIntegratingCommits = true;
|
||||
try {
|
||||
await branchController.mergeUpstream($branch.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isIntegratingCommits = false;
|
||||
}
|
||||
}}>Integrate upstream</Button
|
||||
>
|
||||
{/snippet}
|
||||
</CommitAction>
|
||||
</div>
|
||||
{/if}
|
||||
<InsertEmptyCommitAction isFirst on:click={() => insertBlankCommit($branch.head, 'above')} />
|
||||
|
||||
<!-- LOCAL COMMITS -->
|
||||
{#if $localCommits.length > 0}
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.topDropzone,
|
||||
getReorderDropzoneOffset({ isFirst: true })
|
||||
)}
|
||||
{#each $localCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
type="local"
|
||||
first={idx === 0}
|
||||
branch={$branch}
|
||||
last={idx === $localCommits.length - 1}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||
getReorderDropzoneOffset({
|
||||
isLast: idx + 1 === $localCommits.length,
|
||||
isMiddle: idx + 1 === $localCommits.length
|
||||
})
|
||||
)}
|
||||
|
||||
<div class="commits-group">
|
||||
<InsertEmptyCommitAction
|
||||
isLast={$localAndRemoteCommits.length === 0 && idx + 1 === $localCommits.length}
|
||||
isMiddle={$localAndRemoteCommits.length > 0 && idx + 1 === $localCommits.length}
|
||||
on:click={() => insertBlankCommit(commit.id, 'below')}
|
||||
isFirst
|
||||
on:click={() => insertBlankCommit($branch.head, 'above')}
|
||||
/>
|
||||
{/each}
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.topDropzone,
|
||||
getReorderDropzoneOffset({ isFirst: true })
|
||||
)}
|
||||
{#each $localCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
type="local"
|
||||
first={idx === 0}
|
||||
branch={$branch}
|
||||
last={idx === $localCommits.length - 1}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||
getReorderDropzoneOffset({
|
||||
isLast: idx + 1 === $localCommits.length,
|
||||
isMiddle: idx + 1 === $localCommits.length
|
||||
})
|
||||
)}
|
||||
|
||||
<InsertEmptyCommitAction
|
||||
isLast={idx + 1 === $localCommits.length}
|
||||
on:click={() => insertBlankCommit(commit.id, 'below')}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<CommitAction bottomBorder={hasRemoteCommits}>
|
||||
{#snippet lines()}
|
||||
<LineGroup lineGroup={lineManager.get('local-spacer')} topHeightPx={0} />
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
wide
|
||||
loading={isPushingCommits}
|
||||
on:click={async () => {
|
||||
isPushingCommits = true;
|
||||
try {
|
||||
await branchController.pushBranch($branch.id, $branch.requiresForce);
|
||||
$listingService?.refresh();
|
||||
$prMonitor?.refresh();
|
||||
$checksMonitor?.update();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
isPushingCommits = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$branch.requiresForce ? 'Force push' : 'Push'}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</CommitAction>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- LOCAL AND REMOTE COMMITS -->
|
||||
{#if $localAndRemoteCommits.length > 0}
|
||||
{#each $localAndRemoteCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
type="localAndRemote"
|
||||
first={idx === 0}
|
||||
branch={$branch}
|
||||
last={idx === $localAndRemoteCommits.length - 1}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||
getReorderDropzoneOffset({
|
||||
isMiddle: idx + 1 === $localAndRemoteCommits.length
|
||||
// isLast: idx + 1 === $localAndRemoteCommits.length
|
||||
})
|
||||
)}
|
||||
<InsertEmptyCommitAction
|
||||
isLast={idx + 1 === $localAndRemoteCommits.length}
|
||||
on:click={() => insertBlankCommit(commit.id, 'below')}
|
||||
/>
|
||||
{/each}
|
||||
<div class="commits-group">
|
||||
{#each $localAndRemoteCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
type="localAndRemote"
|
||||
first={idx === 0}
|
||||
branch={$branch}
|
||||
last={idx === $localAndRemoteCommits.length - 1}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{@render reorderDropzone(
|
||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||
getReorderDropzoneOffset({
|
||||
isMiddle: idx + 1 === $localAndRemoteCommits.length
|
||||
})
|
||||
)}
|
||||
<InsertEmptyCommitAction
|
||||
isLast={idx + 1 === $localAndRemoteCommits.length}
|
||||
on:click={() => insertBlankCommit(commit.id, 'below')}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- INTEGRATED COMMITS -->
|
||||
{#if $integratedCommits.length > 0}
|
||||
{#each $integratedCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
type="integrated"
|
||||
first={idx === 0}
|
||||
branch={$branch}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
last={idx === $integratedCommits.length - 1}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
<div class="commits-group">
|
||||
{#each $integratedCommits as commit, idx (commit.id)}
|
||||
<CommitCard
|
||||
{commit}
|
||||
{isUnapplied}
|
||||
type="integrated"
|
||||
first={idx === 0}
|
||||
branch={$branch}
|
||||
isHeadCommit={commit.id === headCommit?.id}
|
||||
last={idx === $integratedCommits.length - 1}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
<LineGroup lineGroup={lineManager.get(commit.id)} {topHeightPx} />
|
||||
{/snippet}
|
||||
</CommitCard>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- BASE -->
|
||||
<div class="base-row-container" class:base-row-container_unfolded={baseIsUnfolded}>
|
||||
<div
|
||||
@ -246,7 +336,8 @@
|
||||
flex-direction: column;
|
||||
background-color: var(--clr-bg-2);
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
/* border-bottom: 1px solid var(--clr-border-2); */
|
||||
border-bottom-left-radius: var(--radius-m);
|
||||
border-bottom-right-radius: var(--radius-m);
|
||||
|
||||
--base-top-margin: 8px;
|
||||
--base-row-height: 20px;
|
||||
@ -264,6 +355,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: var(--base-row-height);
|
||||
border-bottom-left-radius: var(--radius-m);
|
||||
border-bottom-right-radius: var(--radius-m);
|
||||
|
||||
overflow: hidden;
|
||||
transition: height var(--transition-medium);
|
||||
|
@ -3,6 +3,7 @@
|
||||
import Spacer from '../shared/Spacer.svelte';
|
||||
import CommitCard from '$lib/commit/CommitCard.svelte';
|
||||
import { projectMergeUpstreamWarningDismissed } from '$lib/config/config';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { showInfo } from '$lib/notifications/toasts';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Modal from '$lib/shared/Modal.svelte';
|
||||
@ -14,6 +15,7 @@
|
||||
export let base: BaseBranch;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const gitHost = getGitHost();
|
||||
|
||||
const mergeUpstreamWarningDismissed = projectMergeUpstreamWarningDismissed(
|
||||
branchController.projectId
|
||||
@ -61,7 +63,7 @@
|
||||
first={index === 0}
|
||||
last={index === base.upstreamCommits.length - 1}
|
||||
isUnapplied={true}
|
||||
commitUrl={base.commitUrl(commit.id)}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="remote"
|
||||
/>
|
||||
{/each}
|
||||
@ -82,7 +84,7 @@
|
||||
first={index === 0}
|
||||
last={index === base.recentCommits.length - 1}
|
||||
isUnapplied={true}
|
||||
commitUrl={base.commitUrl(commit.id)}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="localAndRemote"
|
||||
/>
|
||||
{/each}
|
||||
|
@ -7,6 +7,7 @@
|
||||
import BranchLane from '$lib/branch/BranchLane.svelte';
|
||||
import { cloneElement } from '$lib/dragging/draggable';
|
||||
import { editor } from '$lib/editorLink/editorLink';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
@ -21,8 +22,9 @@
|
||||
const error = vbranchService.error;
|
||||
const branches = vbranchService.branches;
|
||||
const showHistoryView = persisted(false, 'showHistoryView');
|
||||
const gitHost = getGitHost();
|
||||
|
||||
let dragged: any;
|
||||
let dragged: HTMLDivElement | undefined;
|
||||
let dropZone: HTMLDivElement;
|
||||
let priorPosition = 0;
|
||||
let dropPosition = 0;
|
||||
@ -32,6 +34,7 @@
|
||||
$: if ($error) {
|
||||
$showHistoryView = true;
|
||||
}
|
||||
$: sortedBranches = $branches?.sort((a, b) => a.order - b.order) || [];
|
||||
|
||||
async function openInVSCode() {
|
||||
open(`${$editor}://file${project.vscodePath}/?windowId=_blank`);
|
||||
@ -43,57 +46,60 @@
|
||||
{:else if !$branches}
|
||||
<FullviewLoading />
|
||||
{:else}
|
||||
<div
|
||||
class="board"
|
||||
role="group"
|
||||
data-tauri-drag-region
|
||||
bind:this={dropZone}
|
||||
on:dragover={(e) => {
|
||||
if (!dragged) return;
|
||||
<div class="board">
|
||||
<div
|
||||
role="group"
|
||||
class="branches"
|
||||
data-tauri-drag-region
|
||||
bind:this={dropZone}
|
||||
on:dragover={(e) => {
|
||||
if (!dragged) return;
|
||||
|
||||
e.preventDefault();
|
||||
const children = [...e.currentTarget.children];
|
||||
dropPosition = 0;
|
||||
// We account for the NewBranchDropZone by subtracting 2
|
||||
for (let i = 0; i < children.length - 2; i++) {
|
||||
const pos = children[i].getBoundingClientRect();
|
||||
if (e.clientX > pos.right + dragged.offsetWidth / 2) {
|
||||
dropPosition = i + 1; // Note that this is declared in the <script>
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const idx = children.indexOf(dragged);
|
||||
if (idx !== dropPosition) {
|
||||
idx >= dropPosition
|
||||
? children[dropPosition].before(dragged)
|
||||
: children[dropPosition].after(dragged);
|
||||
}
|
||||
}}
|
||||
on:drop={(e) => {
|
||||
if (!dragged) return;
|
||||
if (!$branches) return;
|
||||
e.preventDefault();
|
||||
if (priorPosition !== dropPosition) {
|
||||
const el = $branches.splice(priorPosition, 1);
|
||||
$branches.splice(dropPosition, 0, ...el);
|
||||
$branches.forEach((branch, i) => {
|
||||
if (branch.order !== i) {
|
||||
branchController.updateBranchOrder(branch.id, i);
|
||||
e.preventDefault();
|
||||
const children = [...e.currentTarget.children];
|
||||
dropPosition = 0;
|
||||
// We account for the NewBranchDropZone by subtracting 2
|
||||
for (let i = 0; i < children.length - 1; i++) {
|
||||
const pos = children[i].getBoundingClientRect();
|
||||
if (e.clientX > pos.left + dragged.offsetWidth / 2) {
|
||||
dropPosition = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each $branches.sort((a, b) => a.order - b.order) as branch (branch.id)}
|
||||
<div
|
||||
role="presentation"
|
||||
aria-label="Branch"
|
||||
tabindex="-1"
|
||||
class="branch draggable-branch"
|
||||
draggable="true"
|
||||
on:mousedown={(e) => (dragHandle = e.target)}
|
||||
on:dragstart={(e) => {
|
||||
}
|
||||
const idx = children.indexOf(dragged);
|
||||
if (idx !== dropPosition) {
|
||||
if (idx >= dropPosition) {
|
||||
children[dropPosition].before(dragged);
|
||||
} else {
|
||||
children[dropPosition].after(dragged);
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:drop={(e) => {
|
||||
if (!dragged) return;
|
||||
if (!$branches) return;
|
||||
dragged.style.opacity = '1';
|
||||
e.preventDefault();
|
||||
if (priorPosition !== dropPosition) {
|
||||
const el = $branches.splice(priorPosition, 1);
|
||||
$branches.splice(dropPosition, 0, ...el);
|
||||
const updates = $branches.map((b, i) => {
|
||||
return { id: b.id, order: i };
|
||||
});
|
||||
branchController.updateBranchOrder(updates);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each sortedBranches as branch (branch.id)}
|
||||
<div
|
||||
role="presentation"
|
||||
aria-label="Branch"
|
||||
tabindex="-1"
|
||||
class="branch draggable-branch"
|
||||
draggable="true"
|
||||
on:mousedown={(e) => (dragHandle = e.target)}
|
||||
on:dragstart={(e) => {
|
||||
if (dragHandle.dataset.dragHandle === undefined) {
|
||||
// We rely on elements with id `drag-handle` to initiate this drag
|
||||
e.preventDefault();
|
||||
@ -107,16 +113,18 @@
|
||||
e.dataTransfer?.setData('text/html', 'd'); // cannot be empty string
|
||||
e.dataTransfer?.setDragImage(clone, e.offsetX, e.offsetY); // Adds the padding
|
||||
dragged = e.currentTarget;
|
||||
dragged.style.opacity = "0.6";
|
||||
priorPosition = Array.from(dropZone.children).indexOf(dragged);
|
||||
}}
|
||||
on:dragend={() => {
|
||||
dragged = undefined;
|
||||
clone?.remove();
|
||||
}}
|
||||
>
|
||||
<BranchLane {branch} />
|
||||
</div>
|
||||
{/each}
|
||||
on:dragend={() => {
|
||||
dragged = undefined;
|
||||
clone?.remove();
|
||||
}}
|
||||
>
|
||||
<BranchLane {branch} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if $branches.length === 0}
|
||||
<div
|
||||
@ -184,7 +192,7 @@
|
||||
{#each ($baseBranch?.recentCommits || []).slice(0, 4) as commit}
|
||||
<a
|
||||
class="empty-board__suggestions__link"
|
||||
href={$baseBranch?.commitUrl(commit.id)}
|
||||
href={$gitHost?.commitUrl(commit.id)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Open in browser"
|
||||
@ -223,6 +231,13 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.branches {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.branch {
|
||||
height: 100%;
|
||||
}
|
||||
@ -360,7 +375,7 @@
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--clr-bg-2);
|
||||
background-color: var(--clr-bg-1-muted);
|
||||
}
|
||||
|
||||
& span {
|
||||
|
@ -3,13 +3,13 @@
|
||||
import Resizer from '../shared/Resizer.svelte';
|
||||
import ScrollableContainer from '../shared/ScrollableContainer.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import CommitCard from '$lib/commit/CommitCard.svelte';
|
||||
import { transformAnyCommit } from '$lib/commitLines/transformers';
|
||||
import FileCard from '$lib/file/FileCard.svelte';
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import { getContext, getContextStore, getContextStoreBySymbol } from '$lib/utils/context';
|
||||
import { getContext, getContextStoreBySymbol } from '$lib/utils/context';
|
||||
import { getMarkdownRenderer } from '$lib/utils/markdown';
|
||||
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
|
||||
import { BranchData, type Branch } from '$lib/vbranches/types';
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
const project = getContext(Project);
|
||||
const remoteBranchService = getContext(RemoteBranchService);
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
const gitHost = getGitHost();
|
||||
|
||||
const fileIdSelection = new FileIdSelection(project.id, writable([]));
|
||||
setContext(FileIdSelection, fileIdSelection);
|
||||
@ -105,7 +105,7 @@
|
||||
>
|
||||
<ScrollableContainer wide>
|
||||
<div class="branch-preview">
|
||||
<BranchPreviewHeader base={$baseBranch} {localBranch} {remoteBranch} {pr} />
|
||||
<BranchPreviewHeader {localBranch} {remoteBranch} {pr} />
|
||||
{#if pr}
|
||||
<div class="card">
|
||||
<div class="card__header text-base-body-14 text-semibold">{pr.title}</div>
|
||||
@ -123,7 +123,7 @@
|
||||
first={index === 0}
|
||||
last={index === remoteCommits.length - 1}
|
||||
{commit}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="remote"
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
@ -138,7 +138,7 @@
|
||||
first={index === 0}
|
||||
last={index === localCommits.length - 1}
|
||||
{commit}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="local"
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
@ -153,7 +153,7 @@
|
||||
first={index === 0}
|
||||
last={index === localAndRemoteCommits.length - 1}
|
||||
{commit}
|
||||
commitUrl={$baseBranch?.commitUrl(commit.id)}
|
||||
commitUrl={$gitHost?.commitUrl(commit.id)}
|
||||
type="localAndRemote"
|
||||
>
|
||||
{#snippet lines(topHeightPx)}
|
||||
|
@ -9,7 +9,12 @@
|
||||
const dispatch = createEventDispatcher<{ click: void }>();
|
||||
</script>
|
||||
|
||||
<div class="container" class:is-last={isLast} class:is-first={isFirst} class:is-middle={isMiddle}>
|
||||
<div
|
||||
class="line-container"
|
||||
class:is-last={isLast}
|
||||
class:is-first={isFirst}
|
||||
class:is-middle={isMiddle}
|
||||
>
|
||||
<div class="hover-target">
|
||||
<Button
|
||||
style="ghost"
|
||||
@ -26,7 +31,7 @@
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.container {
|
||||
.line-container {
|
||||
--height: 14px;
|
||||
--container-margin: calc(var(--height) / 2 * -1);
|
||||
|
||||
@ -89,15 +94,15 @@
|
||||
|
||||
/* MODIFIERS */
|
||||
|
||||
.container.is-last {
|
||||
.line-container.is-last {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.container.is-first {
|
||||
.line-container.is-first {
|
||||
transform: translateY(16px);
|
||||
}
|
||||
|
||||
.container.is-middle {
|
||||
.line-container.is-middle {
|
||||
transform: translateY(6px);
|
||||
}
|
||||
</style>
|
||||
|
@ -90,12 +90,7 @@
|
||||
|
||||
<style lang="postcss">
|
||||
.project-name {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
line-height: 120%;
|
||||
color: var(--clr-scale-ntrl-30);
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.switchrepo__title {
|
||||
|
@ -81,7 +81,7 @@
|
||||
<div class="project-setup">
|
||||
<div class="project-setup__info">
|
||||
<ProjectNameLabel {projectName} />
|
||||
<h3 class="text-base-body-14 text-bold">Target trunk branch</h3>
|
||||
<h3 class="text-base-body-14 text-bold">Target branch</h3>
|
||||
</div>
|
||||
|
||||
<div class="project-setup__fields">
|
||||
|
@ -1,74 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
export enum BranchAction {
|
||||
Push = 'push',
|
||||
Integrate = 'integrate'
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import DropDownButton from '$lib/shared/DropDownButton.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let integrate: boolean; // Integrate upstream option enabled
|
||||
export let projectId: string;
|
||||
|
||||
export let isLoading = false;
|
||||
export let wide = false;
|
||||
export let requiresForce: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher<{ trigger: { action: BranchAction } }>();
|
||||
const preferredAction = persisted<BranchAction>(
|
||||
BranchAction.Push,
|
||||
'projectDefaultAction_' + projectId
|
||||
);
|
||||
|
||||
let dropDown: DropDownButton;
|
||||
let disabled = false;
|
||||
|
||||
$: action = selectAction($preferredAction);
|
||||
$: pushLabel = requiresForce ? 'Force push' : 'Push';
|
||||
$: labels = {
|
||||
[BranchAction.Push]: pushLabel,
|
||||
[BranchAction.Integrate]: 'Integrate upstream'
|
||||
};
|
||||
|
||||
function selectAction(preferredAction: BranchAction) {
|
||||
if (preferredAction === BranchAction.Integrate && integrate) return BranchAction.Integrate;
|
||||
return BranchAction.Push;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropDownButton
|
||||
style="pop"
|
||||
kind="solid"
|
||||
loading={isLoading}
|
||||
bind:this={dropDown}
|
||||
{wide}
|
||||
{disabled}
|
||||
menuPosition="top"
|
||||
on:click={() => {
|
||||
dispatch('trigger', { action });
|
||||
}}
|
||||
>
|
||||
{labels[action]}
|
||||
<ContextMenuSection slot="context-menu">
|
||||
<ContextMenuItem
|
||||
label={labels[BranchAction.Push]}
|
||||
on:click={() => {
|
||||
$preferredAction = BranchAction.Push;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
label={labels[BranchAction.Integrate]}
|
||||
disabled={!integrate}
|
||||
on:click={() => {
|
||||
$preferredAction = BranchAction.Integrate;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
</DropDownButton>
|
43
apps/desktop/src/lib/gitHost/azure/azure.ts
Normal file
43
apps/desktop/src/lib/gitHost/azure/azure.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { AzureBranch } from './azureBranch';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHost } from '../interface/gitHost';
|
||||
|
||||
export const AZURE_DOMAIN = 'dev.azure.com';
|
||||
|
||||
/**
|
||||
* PR support is pending OAuth support in the rust code.
|
||||
*
|
||||
* Follow this issue to stay in the loop:
|
||||
* https://github.com/gitbutlerapp/gitbutler/issues/2651
|
||||
*/
|
||||
export class AzureDevOps implements GitHost {
|
||||
url: string;
|
||||
|
||||
constructor(
|
||||
repo: RepoInfo,
|
||||
private baseBranch: string,
|
||||
private fork?: string
|
||||
) {
|
||||
this.url = `https://${AZURE_DOMAIN}/${repo.organization}/${repo.owner}/_git/${repo.name}`;
|
||||
}
|
||||
|
||||
branch(name: string) {
|
||||
return new AzureBranch(name, this.baseBranch, this.url, this.fork);
|
||||
}
|
||||
|
||||
commitUrl(id: string): string {
|
||||
return `${this.url}/commit/${id}`;
|
||||
}
|
||||
|
||||
listService() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
prService(_baseBranch: string, _upstreamName: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
checksMonitor(_sourceBranch: string) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
11
apps/desktop/src/lib/gitHost/azure/azureBranch.ts
Normal file
11
apps/desktop/src/lib/gitHost/azure/azureBranch.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { GitHostBranch } from '../interface/gitHostBranch';
|
||||
|
||||
export class AzureBranch implements GitHostBranch {
|
||||
readonly url: string;
|
||||
constructor(name: string, baseBranch: string, baseUrl: string, fork?: string) {
|
||||
if (fork) {
|
||||
name = `${fork}:${name}`;
|
||||
}
|
||||
this.url = `${baseUrl}/branchCompare?baseVersion=GB${baseBranch}&targetVersion=GB${name}`;
|
||||
}
|
||||
}
|
48
apps/desktop/src/lib/gitHost/bitbucket/bitbucket.ts
Normal file
48
apps/desktop/src/lib/gitHost/bitbucket/bitbucket.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { BitBucketBranch } from './bitbucketBranch';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHost } from '../interface/gitHost';
|
||||
import type { DetailedPullRequest } from '../interface/types';
|
||||
|
||||
export type PrAction = 'creating_pr';
|
||||
export type PrState = { busy: boolean; branchId: string; action?: PrAction };
|
||||
export type PrCacheKey = { value: DetailedPullRequest | undefined; fetchedAt: Date };
|
||||
|
||||
export const BITBUCKET_DOMAIN = 'bitbucket.org';
|
||||
|
||||
/**
|
||||
* PR support is pending OAuth support in the rust code.
|
||||
*
|
||||
* Follow this issue to stay in the loop:
|
||||
* https://github.com/gitbutlerapp/gitbutler/issues/3252
|
||||
*/
|
||||
export class BitBucket implements GitHost {
|
||||
webUrl: string;
|
||||
|
||||
constructor(
|
||||
repo: RepoInfo,
|
||||
private baseBranch: string,
|
||||
private fork?: string
|
||||
) {
|
||||
this.webUrl = `https://${BITBUCKET_DOMAIN}/${repo.owner}/${repo.name}`;
|
||||
}
|
||||
|
||||
branch(name: string) {
|
||||
return new BitBucketBranch(name, this.baseBranch, this.webUrl, this.fork);
|
||||
}
|
||||
|
||||
commitUrl(id: string): string {
|
||||
return `${this.webUrl}/commits/${id}`;
|
||||
}
|
||||
|
||||
listService() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
prService(_baseBranch: string, _upstreamName: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
checksMonitor(_sourceBranch: string) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
11
apps/desktop/src/lib/gitHost/bitbucket/bitbucketBranch.ts
Normal file
11
apps/desktop/src/lib/gitHost/bitbucket/bitbucketBranch.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { GitHostBranch } from '../interface/gitHostBranch';
|
||||
|
||||
export class BitBucketBranch implements GitHostBranch {
|
||||
readonly url: string;
|
||||
constructor(name: string, baseBranch: string, baseUrl: string, fork?: string) {
|
||||
if (fork) {
|
||||
name = `${fork}:${name}`;
|
||||
}
|
||||
this.url = `${baseUrl}/branch/${name}?dest=${baseBranch}`;
|
||||
}
|
||||
}
|
@ -7,11 +7,14 @@ describe.concurrent('DefaultgitHostFactory', () => {
|
||||
test('Create GitHub service', async () => {
|
||||
const monitorFactory = new DefaultGitHostFactory(new Octokit());
|
||||
expect(
|
||||
monitorFactory.build({
|
||||
provider: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
})
|
||||
monitorFactory.build(
|
||||
{
|
||||
source: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
},
|
||||
'some-base'
|
||||
)
|
||||
).instanceOf(GitHub);
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { GitHub } from './github/github';
|
||||
import { AZURE_DOMAIN, AzureDevOps } from './azure/azure';
|
||||
import { BitBucket, BITBUCKET_DOMAIN } from './bitbucket/bitbucket';
|
||||
import { GitHub, GITHUB_DOMAIN } from './github/github';
|
||||
import { GitLab, GITLAB_DOMAIN } from './gitlab/gitlab';
|
||||
import { ProjectMetrics } from '$lib/metrics/projectMetrics';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHost } from './interface/gitHost';
|
||||
@ -7,17 +10,26 @@ import type { Octokit } from '@octokit/rest';
|
||||
// Used on a branch level to acquire the right kind of merge request / checks
|
||||
// monitoring service.
|
||||
export interface GitHostFactory {
|
||||
build(repo: RepoInfo): GitHost | undefined;
|
||||
build(repo: RepoInfo, baseBranch: string): GitHost | undefined;
|
||||
}
|
||||
|
||||
export class DefaultGitHostFactory implements GitHostFactory {
|
||||
constructor(private octokit: Octokit | undefined) {}
|
||||
|
||||
build(repo: RepoInfo) {
|
||||
switch (repo.provider) {
|
||||
case 'github.com':
|
||||
if (!this.octokit) throw new Error('Octokit not available');
|
||||
return new GitHub(this.octokit, repo, new ProjectMetrics());
|
||||
build(repo: RepoInfo, baseBranch: string, fork?: RepoInfo) {
|
||||
const source = repo.source;
|
||||
const forkStr = fork ? `${fork.owner}:${fork.name}` : undefined;
|
||||
if (source.includes(GITHUB_DOMAIN)) {
|
||||
return new GitHub(repo, baseBranch, forkStr, this.octokit, new ProjectMetrics());
|
||||
}
|
||||
if (source.includes(GITLAB_DOMAIN)) {
|
||||
return new GitLab(repo, baseBranch, forkStr);
|
||||
}
|
||||
if (source.includes(BITBUCKET_DOMAIN)) {
|
||||
return new BitBucket(repo, baseBranch, forkStr);
|
||||
}
|
||||
if (source.includes(AZURE_DOMAIN)) {
|
||||
return new AzureDevOps(repo, baseBranch, forkStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
17
apps/desktop/src/lib/gitHost/github/github.test.ts
Normal file
17
apps/desktop/src/lib/gitHost/github/github.test.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { GitHub } from './github';
|
||||
import { expect, test, describe } from 'vitest';
|
||||
|
||||
describe.concurrent('GitHub', () => {
|
||||
const id = 'some-branch';
|
||||
const repo = {
|
||||
source: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
};
|
||||
|
||||
test('commit url', async () => {
|
||||
const gh = new GitHub(repo);
|
||||
const url = gh.commitUrl(id);
|
||||
expect(url).toMatch(new RegExp(`/${id}$`));
|
||||
});
|
||||
});
|
@ -1,3 +1,4 @@
|
||||
import { GitHubBranch } from './githubBranch';
|
||||
import { GitHubChecksMonitor } from './githubChecksMonitor';
|
||||
import { GitHubListingService } from './githubListingService';
|
||||
import { GitHubPrService } from './githubPrService';
|
||||
@ -5,28 +6,51 @@ import { Octokit } from '@octokit/rest';
|
||||
import type { ProjectMetrics } from '$lib/metrics/projectMetrics';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHost } from '../interface/gitHost';
|
||||
import type { DetailedPullRequest } from '../interface/types';
|
||||
|
||||
export type PrAction = 'creating_pr';
|
||||
export type PrState = { busy: boolean; branchId: string; action?: PrAction };
|
||||
export type PrCacheKey = { value: DetailedPullRequest | undefined; fetchedAt: Date };
|
||||
export const GITHUB_DOMAIN = 'github.com';
|
||||
|
||||
export class GitHub implements GitHost {
|
||||
baseUrl: string;
|
||||
|
||||
constructor(
|
||||
private octokit: Octokit,
|
||||
private repo: RepoInfo,
|
||||
private baseBranch?: string,
|
||||
private fork?: string,
|
||||
private octokit?: Octokit,
|
||||
private projectMetrics?: ProjectMetrics
|
||||
) {}
|
||||
) {
|
||||
this.baseUrl = `https://${GITHUB_DOMAIN}/${repo.owner}/${repo.name}`;
|
||||
}
|
||||
|
||||
listService() {
|
||||
if (!this.octokit) {
|
||||
return;
|
||||
}
|
||||
return new GitHubListingService(this.octokit, this.repo, this.projectMetrics);
|
||||
}
|
||||
|
||||
prService(baseBranch: string, upstreamName: string) {
|
||||
if (!this.octokit) {
|
||||
return;
|
||||
}
|
||||
return new GitHubPrService(this.octokit, this.repo, baseBranch, upstreamName);
|
||||
}
|
||||
|
||||
checksMonitor(sourceBranch: string) {
|
||||
if (!this.octokit) {
|
||||
return;
|
||||
}
|
||||
return new GitHubChecksMonitor(this.octokit, this.repo, sourceBranch);
|
||||
}
|
||||
|
||||
branch(name: string) {
|
||||
if (!this.baseBranch) {
|
||||
return;
|
||||
}
|
||||
return new GitHubBranch(name, this.baseBranch, this.baseUrl, this.fork);
|
||||
}
|
||||
|
||||
commitUrl(id: string): string {
|
||||
return `${this.baseUrl}/commit/${id}`;
|
||||
}
|
||||
}
|
||||
|
26
apps/desktop/src/lib/gitHost/github/githubBranch.test.ts
Normal file
26
apps/desktop/src/lib/gitHost/github/githubBranch.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { GitHub } from './github';
|
||||
import { expect, test, describe } from 'vitest';
|
||||
|
||||
// TODO: Rewrite this proof-of-concept into something valuable.
|
||||
describe.concurrent('GitHubBranch', () => {
|
||||
const name = 'some-branch';
|
||||
const base = 'some-base';
|
||||
const repo = {
|
||||
source: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
};
|
||||
|
||||
test('branch compare url', async () => {
|
||||
const gh = new GitHub(repo, base);
|
||||
const branch = gh.branch(name);
|
||||
expect(branch?.url).toMatch(new RegExp(`...${name}$`));
|
||||
});
|
||||
|
||||
test('fork compare url', async () => {
|
||||
const fork = `${repo.owner}:${repo.name}`;
|
||||
const gh = new GitHub(repo, base, fork);
|
||||
const branch = gh.branch(name);
|
||||
expect(branch?.url).toMatch(new RegExp(`...${fork}:${name}$`));
|
||||
});
|
||||
});
|
11
apps/desktop/src/lib/gitHost/github/githubBranch.ts
Normal file
11
apps/desktop/src/lib/gitHost/github/githubBranch.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { GitHostBranch } from '../interface/gitHostBranch';
|
||||
|
||||
export class GitHubBranch implements GitHostBranch {
|
||||
readonly url: string;
|
||||
constructor(name: string, baseBranch: string, baseUrl: string, fork?: string) {
|
||||
if (fork) {
|
||||
name = `${fork}:${name}`;
|
||||
}
|
||||
this.url = `${baseUrl}/compare/${baseBranch}...${name}`;
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ type CheckSuites =
|
||||
describe.concurrent('GitHubChecksMonitor', () => {
|
||||
let octokit: Octokit;
|
||||
let gh: GitHub;
|
||||
let monitor: GitHostChecksMonitor;
|
||||
let monitor: GitHostChecksMonitor | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@ -28,11 +28,16 @@ describe.concurrent('GitHubChecksMonitor', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
octokit = new Octokit();
|
||||
gh = new GitHub(octokit, {
|
||||
provider: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
});
|
||||
gh = new GitHub(
|
||||
{
|
||||
source: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
octokit
|
||||
);
|
||||
monitor = gh.checksMonitor('upstream-branch');
|
||||
});
|
||||
|
||||
@ -50,11 +55,11 @@ describe.concurrent('GitHubChecksMonitor', () => {
|
||||
} as SuitesResponse)
|
||||
);
|
||||
|
||||
await monitor.update();
|
||||
await monitor?.update();
|
||||
expect(listForRef).toHaveBeenCalledOnce();
|
||||
expect(listSuitesForRef).toHaveBeenCalledOnce();
|
||||
|
||||
const checks = get(monitor.status);
|
||||
const checks = monitor ? get(monitor?.status) : undefined;
|
||||
expect(checks).toBeNull();
|
||||
});
|
||||
|
||||
@ -76,10 +81,10 @@ describe.concurrent('GitHubChecksMonitor', () => {
|
||||
}
|
||||
} as ChecksResponse)
|
||||
);
|
||||
await monitor.update();
|
||||
await monitor?.update();
|
||||
expect(mock).toHaveBeenCalledOnce();
|
||||
|
||||
let status = monitor.getLastStatus();
|
||||
let status = monitor?.getLastStatus();
|
||||
expect(status?.finished).toBeFalsy();
|
||||
|
||||
// Verify that checks are re-fetchd after some timeout.
|
||||
@ -108,7 +113,7 @@ describe.concurrent('GitHubChecksMonitor', () => {
|
||||
);
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
expect(mock2).toHaveBeenCalledOnce();
|
||||
status = monitor.getLastStatus();
|
||||
status = monitor?.getLastStatus();
|
||||
expect(status?.completed).toBeTruthy();
|
||||
|
||||
// Set time to be above minimum age for polling to be stopped.
|
||||
|
@ -9,14 +9,14 @@ type PrListResponse = RestEndpointMethodTypes['pulls']['list']['response'];
|
||||
|
||||
describe.concurrent('GitHubListingService', () => {
|
||||
const repoInfo = {
|
||||
provider: 'github.com',
|
||||
source: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
};
|
||||
|
||||
let octokit: Octokit;
|
||||
let gh: GitHub;
|
||||
let service: GitHostListingService;
|
||||
let service: GitHostListingService | undefined;
|
||||
let projectMetrics: ProjectMetrics;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -31,7 +31,7 @@ describe.concurrent('GitHubListingService', () => {
|
||||
octokit = new Octokit();
|
||||
projectMetrics = new ProjectMetrics();
|
||||
|
||||
gh = new GitHub(octokit, repoInfo, projectMetrics);
|
||||
gh = new GitHub(repoInfo, 'some-base', undefined, octokit, projectMetrics);
|
||||
service = gh.listService();
|
||||
});
|
||||
|
||||
@ -42,9 +42,9 @@ describe.concurrent('GitHubListingService', () => {
|
||||
data: [{ title, labels: [] as Labels }]
|
||||
} as PrListResponse)
|
||||
);
|
||||
const prs = await service.fetch();
|
||||
expect(prs.length).toEqual(1);
|
||||
expect(prs[0].title).toEqual(title);
|
||||
const prs = await service?.fetch();
|
||||
expect(prs?.length).toEqual(1);
|
||||
expect(prs?.[0].title).toEqual(title);
|
||||
|
||||
const metrics = projectMetrics.getMetrics();
|
||||
expect(metrics['pr_count']?.value).toEqual(1);
|
||||
|
@ -8,8 +8,8 @@ import type { GitHostPrService } from '../interface/gitHostPrService';
|
||||
describe.concurrent('GitHubPrMonitor', () => {
|
||||
let octokit: Octokit;
|
||||
let gh: GitHub;
|
||||
let service: GitHostPrService;
|
||||
let monitor: GitHostPrMonitor;
|
||||
let service: GitHostPrService | undefined;
|
||||
let monitor: GitHostPrMonitor | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
@ -21,13 +21,18 @@ describe.concurrent('GitHubPrMonitor', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
octokit = new Octokit();
|
||||
gh = new GitHub(octokit, {
|
||||
provider: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
});
|
||||
gh = new GitHub(
|
||||
{
|
||||
source: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
octokit
|
||||
);
|
||||
service = gh.prService('base-branch', 'upstream-branch');
|
||||
monitor = service.prMonitor(123);
|
||||
monitor = service?.prMonitor(123);
|
||||
});
|
||||
|
||||
test('should run on set interval', async () => {
|
||||
@ -36,13 +41,13 @@ describe.concurrent('GitHubPrMonitor', () => {
|
||||
data: { title: 'PR Title' }
|
||||
} as RestEndpointMethodTypes['pulls']['get']['response'])
|
||||
);
|
||||
const unsubscribe = monitor.pr.subscribe(() => {});
|
||||
const unsubscribe = monitor?.pr.subscribe(() => {});
|
||||
expect(get).toHaveBeenCalledOnce();
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(get).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Unsubscribing should cancel the interval.
|
||||
unsubscribe();
|
||||
unsubscribe?.();
|
||||
vi.advanceTimersToNextTimer();
|
||||
expect(get).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
@ -7,15 +7,20 @@ import type { GitHostPrService as GitHubPrService } from '../interface/gitHostPr
|
||||
describe.concurrent('GitHubPrService', () => {
|
||||
let octokit: Octokit;
|
||||
let gh: GitHub;
|
||||
let service: GitHubPrService;
|
||||
let service: GitHubPrService | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
octokit = new Octokit();
|
||||
gh = new GitHub(octokit, {
|
||||
provider: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
});
|
||||
gh = new GitHub(
|
||||
{
|
||||
source: 'github.com',
|
||||
name: 'test-repo',
|
||||
owner: 'test-owner'
|
||||
},
|
||||
'main',
|
||||
undefined,
|
||||
octokit
|
||||
);
|
||||
service = gh.prService('base-branch', 'upstream-branch');
|
||||
});
|
||||
|
||||
@ -26,7 +31,7 @@ describe.concurrent('GitHubPrService', () => {
|
||||
data: { title }
|
||||
} as RestEndpointMethodTypes['pulls']['get']['response'])
|
||||
);
|
||||
const pr = await service.get(123);
|
||||
const pr = await service?.get(123);
|
||||
expect(pr?.title).equal(title);
|
||||
});
|
||||
});
|
||||
|
48
apps/desktop/src/lib/gitHost/gitlab/gitlab.ts
Normal file
48
apps/desktop/src/lib/gitHost/gitlab/gitlab.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { GitLabBranch } from './gitlabBranch';
|
||||
import type { RepoInfo } from '$lib/url/gitUrl';
|
||||
import type { GitHost } from '../interface/gitHost';
|
||||
import type { DetailedPullRequest } from '../interface/types';
|
||||
|
||||
export type PrAction = 'creating_pr';
|
||||
export type PrState = { busy: boolean; branchId: string; action?: PrAction };
|
||||
export type PrCacheKey = { value: DetailedPullRequest | undefined; fetchedAt: Date };
|
||||
|
||||
export const GITLAB_DOMAIN = 'gitlab.com';
|
||||
|
||||
/**
|
||||
* PR support is pending OAuth support in the rust code.
|
||||
*
|
||||
* Follow this issue to stay in the loop:
|
||||
* https://github.com/gitbutlerapp/gitbutler/issues/2511
|
||||
*/
|
||||
export class GitLab implements GitHost {
|
||||
webUrl: string;
|
||||
|
||||
constructor(
|
||||
repo: RepoInfo,
|
||||
private baseBranch: string,
|
||||
private fork?: string
|
||||
) {
|
||||
this.webUrl = `https://${GITLAB_DOMAIN}/${repo.owner}/${repo.name}`;
|
||||
}
|
||||
|
||||
branch(name: string) {
|
||||
return new GitLabBranch(name, this.baseBranch, this.webUrl, this.fork);
|
||||
}
|
||||
|
||||
commitUrl(id: string): string {
|
||||
return `${this.webUrl}/-/commit/${id}`;
|
||||
}
|
||||
|
||||
listService() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
prService(_baseBranch: string, _upstreamName: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
checksMonitor(_sourceBranch: string) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
11
apps/desktop/src/lib/gitHost/gitlab/gitlabBranch.ts
Normal file
11
apps/desktop/src/lib/gitHost/gitlab/gitlabBranch.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { GitHostBranch } from '../interface/gitHostBranch';
|
||||
|
||||
export class GitLabBranch implements GitHostBranch {
|
||||
readonly url: string;
|
||||
constructor(name: string, baseBranch: string, baseUrl: string, fork?: string) {
|
||||
if (fork) {
|
||||
name = `${fork}:${name}`;
|
||||
}
|
||||
this.url = `${baseUrl}/-/compare/${baseBranch}...${name}`;
|
||||
}
|
||||
}
|
@ -1,12 +1,24 @@
|
||||
import { buildContextStore } from '$lib/utils/context';
|
||||
import type { GitHostBranch } from './gitHostBranch';
|
||||
import type { GitHostChecksMonitor } from './gitHostChecksMonitor';
|
||||
import type { GitHostListingService } from './gitHostListingService';
|
||||
import type { GitHostPrService } from './gitHostPrService';
|
||||
|
||||
export interface GitHost {
|
||||
listService(): GitHostListingService;
|
||||
prService(baseBranch: string, upstreamName: string): GitHostPrService;
|
||||
checksMonitor(sourceBranch: string): GitHostChecksMonitor;
|
||||
// Lists PRs for the repo.
|
||||
listService(): GitHostListingService | undefined;
|
||||
|
||||
// Detailed information about a specific PR.
|
||||
prService(baseBranch: string, upstreamName: string): GitHostPrService | undefined;
|
||||
|
||||
// Results from CI check-runs.
|
||||
checksMonitor(branchName: string): GitHostChecksMonitor | undefined;
|
||||
|
||||
// Host specific branch information.
|
||||
branch(name: string): GitHostBranch | undefined;
|
||||
|
||||
// Web URL for a commit.
|
||||
commitUrl(id: string): string;
|
||||
}
|
||||
|
||||
export const [getGitHost, createGitHostStore] = buildContextStore<GitHost | undefined>(
|
||||
|
3
apps/desktop/src/lib/gitHost/interface/gitHostBranch.ts
Normal file
3
apps/desktop/src/lib/gitHost/interface/gitHostBranch.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface GitHostBranch {
|
||||
url: string;
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import { buildContextStore } from '$lib/utils/context';
|
||||
import type { PullRequest } from './types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
export const [getGitHostListingService, createGitHostListingServiceStore] = buildContextStore<
|
||||
GitHostListingService | undefined
|
||||
>('gitHostListingService');
|
||||
|
||||
export interface GitHostListingService {
|
||||
prs: Writable<PullRequest[]>;
|
||||
prs: Readable<PullRequest[]>;
|
||||
fetch(): Promise<PullRequest[]>;
|
||||
refresh(): Promise<void>;
|
||||
}
|
||||
|
@ -94,7 +94,7 @@
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: max-content;
|
||||
font-family: monospace;
|
||||
font-family: var(--mono-font-family);
|
||||
background-color: var(--clr-bg-1);
|
||||
white-space: pre;
|
||||
tab-size: var(--tab-size);
|
||||
|
@ -21,7 +21,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
use:tooltip={isNavCollapsed ? 'Trunk' : ''}
|
||||
use:tooltip={isNavCollapsed ? 'Target' : ''}
|
||||
on:mousedown={async () => await goto(`/${project.id}/base`)}
|
||||
class="base-branch-card"
|
||||
class:selected
|
||||
@ -39,7 +39,10 @@
|
||||
{#if !isNavCollapsed}
|
||||
<div class="content">
|
||||
<div class="button-head">
|
||||
<span class="text-base-14 text-semibold trunk-label">Trunk</span>
|
||||
<span
|
||||
use:tooltip={'The branch that your Workspace virtual branches are based on and will be merged into.'}
|
||||
class="text-base-14 text-semibold trunk-label">Target</span
|
||||
>
|
||||
{#if ($base?.behind || 0) > 0}
|
||||
<Badge count={$base?.behind || 0} help="Unmerged upstream commits" />
|
||||
{/if}
|
||||
|
@ -228,8 +228,8 @@
|
||||
<SectionCard orientation="row" labelFor="allowForcePush">
|
||||
<svelte:fragment slot="title">Allow force pushing</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
Force pushing allows GitButler to override branches even if they were pushed to remote. We
|
||||
will never force push to the trunk.
|
||||
Force pushing allows GitButler to override branches even if they were pushed to remote.
|
||||
GitButler will never force push to the target branch.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
|
@ -1,16 +1,18 @@
|
||||
import gitUrlParse from 'git-url-parse';
|
||||
|
||||
export type RepoInfo = {
|
||||
provider: string;
|
||||
source: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
organization?: string;
|
||||
};
|
||||
|
||||
export function parseRemoteUrl(url: string): RepoInfo {
|
||||
const { source, name, owner } = gitUrlParse(url);
|
||||
const { source, name, owner, organization } = gitUrlParse(url);
|
||||
return {
|
||||
provider: source,
|
||||
source,
|
||||
name,
|
||||
owner
|
||||
owner,
|
||||
organization
|
||||
};
|
||||
}
|
||||
|
@ -140,11 +140,11 @@ export class BranchController {
|
||||
}
|
||||
}
|
||||
|
||||
async updateBranchOrder(branchId: string, order: number) {
|
||||
async updateBranchOrder(branches: { id: string; order: number }[]) {
|
||||
try {
|
||||
await invoke<void>('update_virtual_branch', {
|
||||
await invoke<void>('update_branch_order', {
|
||||
projectId: this.projectId,
|
||||
branch: { id: branchId, order }
|
||||
branches
|
||||
});
|
||||
} catch (err) {
|
||||
showError('Failed to update branch order', err);
|
||||
|
@ -86,10 +86,12 @@ export class VirtualBranchService {
|
||||
|
||||
private logMetrics(branches: VirtualBranch[]) {
|
||||
try {
|
||||
const hunks = branches.flatMap((branch) => branch.files).flatMap((file) => file.hunks);
|
||||
const files = branches.flatMap((branch) => branch.files);
|
||||
const hunks = files.flatMap((file) => file.hunks);
|
||||
const lockedHunks = hunks.filter((hunk) => hunk.locked);
|
||||
this.projectMetrics.setMetric('hunk_count', hunks.length);
|
||||
this.projectMetrics.setMetric('locked_hunk_count', lockedHunks.length);
|
||||
this.projectMetrics.setMetric('file_count', files.length);
|
||||
this.projectMetrics.setMetric('virtual_branch_count', branches.length);
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
|
@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../styles/main.css';
|
||||
import '@gitbutler/ui/fonts.css';
|
||||
import '@gitbutler/ui/main.css';
|
||||
import '../styles.css';
|
||||
|
||||
import { PromptService as AIPromptService } from '$lib/ai/promptService';
|
||||
import { AIService } from '$lib/ai/service';
|
||||
|
@ -52,6 +52,7 @@
|
||||
const branchesError = $derived(vbranchService.branchesError);
|
||||
const baseBranch = $derived(baseBranchService.base);
|
||||
const remoteUrl = $derived($baseBranch?.remoteUrl);
|
||||
const forkUrl = $derived($baseBranch?.pushRemoteUrl);
|
||||
const user = $derived(userService.user);
|
||||
const accessToken = $derived($user?.github_access_token);
|
||||
const baseError = $derived(baseBranchService.error);
|
||||
@ -77,9 +78,11 @@
|
||||
const octokit = $derived(accessToken ? octokitFromAccessToken(accessToken) : undefined);
|
||||
const gitHostFactory = $derived(octokit ? new DefaultGitHostFactory(octokit) : undefined);
|
||||
const repoInfo = $derived(remoteUrl ? parseRemoteUrl(remoteUrl) : undefined);
|
||||
const forkInfo = $derived(forkUrl && forkUrl !== remoteUrl ? parseRemoteUrl(forkUrl) : undefined);
|
||||
const baseBranchName = $derived($baseBranch?.shortName);
|
||||
|
||||
const listServiceStore = createGitHostListingServiceStore(undefined);
|
||||
const githubRepoServiceStore = createGitHostStore(undefined);
|
||||
const gitHostStore = createGitHostStore(undefined);
|
||||
const branchServiceStore = createBranchServiceStore(undefined);
|
||||
|
||||
// Refresh base branch if git fetch event is detected.
|
||||
@ -102,11 +105,14 @@
|
||||
});
|
||||
|
||||
$effect.pre(() => {
|
||||
const gitHost = repoInfo ? gitHostFactory?.build(repoInfo) : undefined;
|
||||
const gitHost =
|
||||
repoInfo && baseBranchName
|
||||
? gitHostFactory?.build(repoInfo, baseBranchName, forkInfo)
|
||||
: undefined;
|
||||
const ghListService = gitHost?.listService();
|
||||
|
||||
listServiceStore.set(ghListService);
|
||||
githubRepoServiceStore.set(gitHost);
|
||||
gitHostStore.set(gitHost);
|
||||
branchServiceStore.set(
|
||||
new BranchService(
|
||||
vbranchService,
|
||||
|
@ -8,11 +8,11 @@
|
||||
import PullRequestPreview from '$lib/components/PullRequestPreview.svelte';
|
||||
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
const gitHostListing = getGitHostListingService();
|
||||
let prs = $state<Writable<PullRequest[]>>();
|
||||
let prs = $state<Readable<PullRequest[]> | undefined>();
|
||||
let pr = $state<PullRequest>();
|
||||
$effect.pre(() => {
|
||||
prs = $gitHostListing?.prs;
|
||||
|
@ -15,8 +15,8 @@
|
||||
<SectionCard labelFor="baseBranchSwitching" orientation="row">
|
||||
<svelte:fragment slot="title">Switching the target branch</svelte:fragment>
|
||||
<svelte:fragment slot="caption">
|
||||
This allows changing of the target branch (trunk) after the initial project setup from within
|
||||
the project settings. Not fully tested yet, use with caution.
|
||||
This allows changing of the target branch after the initial project setup from within the
|
||||
project settings. Not fully tested yet, use with caution.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle
|
||||
|
@ -1,83 +1,3 @@
|
||||
@layer reset;
|
||||
|
||||
@import './reset.css';
|
||||
@import 'inter-ui/inter.css';
|
||||
@import './fonts.css';
|
||||
@import './diff.css';
|
||||
@import './syntax-highlighting.css';
|
||||
@import './tokens.css';
|
||||
@import './text-classes.css';
|
||||
@import './card.css';
|
||||
@import './tooltip.css';
|
||||
@import './text-input.css';
|
||||
@import './commit-lines.css';
|
||||
@import './markdown.css';
|
||||
@import './draggable.css';
|
||||
|
||||
/* CSS VARIABLES */
|
||||
:root {
|
||||
--transition-fast: 0.06s ease-in-out;
|
||||
--transition-medium: 0.15s ease-in-out;
|
||||
--transition-slow: 0.2s ease-in-out;
|
||||
|
||||
/* Z-index */
|
||||
--z-ground: 1;
|
||||
--z-lifted: 2;
|
||||
--z-floating: 3;
|
||||
--z-modal: 4;
|
||||
--z-tooltip: 9;
|
||||
--z-blocker: 10;
|
||||
|
||||
/* TODO: add focus color */
|
||||
--focus-color: var(--clr-scale-pop-50);
|
||||
--resizer-color: var(--clr-scale-pop-50);
|
||||
}
|
||||
|
||||
/* scrollbar helpers */
|
||||
.hide-native-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* custom scrollbar */
|
||||
.scrollbar,
|
||||
pre {
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transaparent;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transaparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--clr-border-1);
|
||||
background-clip: padding-box;
|
||||
border-radius: 12px;
|
||||
border: 4px solid rgba(0, 0, 0, 0);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
|
||||
@ -86,6 +6,11 @@ pre {
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--clr-text-1);
|
||||
background-color: var(--clr-bg-2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents elements within drop-zones from firing mouse events, making
|
||||
* it much easier to manage in/out/over/leave events since they fire less
|
||||
@ -96,7 +21,6 @@ pre {
|
||||
}
|
||||
|
||||
/* FOCUS STATE */
|
||||
|
||||
.focus-state {
|
||||
&:focus-within {
|
||||
outline: 1px solid transaparent;
|
||||
@ -116,7 +40,7 @@ pre {
|
||||
|
||||
/* CODE */
|
||||
.code-string {
|
||||
font-family: 'Spline Sans Mono', monospace;
|
||||
font-family: var(--mono-font-family);
|
||||
border-radius: var(--radius-s);
|
||||
background: var(--clr-scale-ntrl-80);
|
||||
padding: 1px 4px;
|
@ -1,33 +0,0 @@
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--clr-bg-1);
|
||||
}
|
||||
|
||||
.card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
}
|
||||
|
||||
.card__title {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card__footer {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 16px;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'PP Editorial New';
|
||||
src: url('/fonts/PPEditorialNew-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Spline Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SplineSansMono-Regular.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
|
||||
U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||
U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Spline Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SplineSansMono-Medium.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
|
||||
U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||
U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Spline Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/SplineSansMono-Semibold.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
|
||||
U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
|
||||
U+FFFD;
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
/* BOILERPLATE CSS */
|
||||
/* reset all styles */
|
||||
@layer reset {
|
||||
/* base */
|
||||
*,
|
||||
:after,
|
||||
:before {
|
||||
box-sizing: border-box;
|
||||
border: 0 solid;
|
||||
}
|
||||
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: inherit;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-y: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: inherit;
|
||||
|
||||
color: var(--clr-text-1);
|
||||
background-color: var(--clr-bg-2);
|
||||
|
||||
/* optimise font rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* elements */
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family:
|
||||
Söhne Mono,
|
||||
monospace;
|
||||
font-feature-settings: normal;
|
||||
font-variation-settings: normal;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
font-size: 100%;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input:where([type='button']),
|
||||
input:where([type='reset']),
|
||||
input:where([type='submit']) {
|
||||
-webkit-appearance: button;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
dd,
|
||||
dl,
|
||||
figure,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
menu,
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
[role='button'],
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
embed,
|
||||
iframe,
|
||||
img,
|
||||
object,
|
||||
svg,
|
||||
video {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
.token-variable {
|
||||
color: #8953800;
|
||||
}
|
||||
|
||||
.token-property {
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
.token-type {
|
||||
color: #116329;
|
||||
}
|
||||
|
||||
.token-variable-special {
|
||||
color: #953800;
|
||||
}
|
||||
|
||||
.token-definition {
|
||||
color: #953800;
|
||||
}
|
||||
|
||||
/* .token-builtin {
|
||||
color: #d3869b;
|
||||
} */
|
||||
|
||||
.token-number {
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
.token-string {
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
.token-string-special {
|
||||
color: #0a3069;
|
||||
}
|
||||
|
||||
/* .token-atom {
|
||||
color: #0a3069;
|
||||
} */
|
||||
|
||||
.token-keyword {
|
||||
color: #cf222e;
|
||||
}
|
||||
|
||||
.token-comment {
|
||||
color: #6e7781;
|
||||
}
|
||||
|
||||
.token-meta {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.token-invalid {
|
||||
color: #82071e;
|
||||
}
|
||||
|
||||
.token-tag {
|
||||
color: #116329;
|
||||
}
|
||||
|
||||
.token-attribute {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.token-attribute-value {
|
||||
color: var(--color-token-attribute-value);
|
||||
}
|
||||
|
||||
.token-inserted {
|
||||
color: #116329;
|
||||
}
|
||||
|
||||
.token-deleted {
|
||||
color: #82071e;
|
||||
}
|
||||
|
||||
.token-heading {
|
||||
color: var(--color-token-variable-special);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token-link {
|
||||
color: var(--color-token-variable-special);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.token-strikethrough {
|
||||
text-decoration: strike-through;
|
||||
}
|
||||
|
||||
.token-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.token-variable {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-property {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-type {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.token-variable-special {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-definition {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
/* .token-builtin {
|
||||
color: #d3869b;
|
||||
} */
|
||||
|
||||
.token-number {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.token-string {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.token-string-special {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
/* .token-atom {
|
||||
color: #0a3069;
|
||||
} */
|
||||
|
||||
.token-keyword {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.token-comment {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.token-meta {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.token-invalid {
|
||||
color: #ffa198;
|
||||
}
|
||||
|
||||
.token-tag {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.token-attribute {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.token-attribute-value {
|
||||
color: var(--color-token-attribute-value);
|
||||
}
|
||||
|
||||
.token-inserted {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.token-deleted {
|
||||
color: #ffa198;
|
||||
}
|
||||
|
||||
.token-heading {
|
||||
color: var(--color-token-variable-special);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token-link {
|
||||
color: var(--color-token-variable-special);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.token-strikethrough {
|
||||
text-decoration: strike-through;
|
||||
}
|
||||
|
||||
.token-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -38,7 +38,10 @@ export default defineConfig({
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true
|
||||
strictPort: true,
|
||||
fs: {
|
||||
strict: false
|
||||
}
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@gitbutler/cloud",
|
||||
"name": "@gitbutler/web",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
@ -8,8 +8,8 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
|
@ -24,6 +24,7 @@ gitbutler-commit.workspace = true
|
||||
gitbutler-url.workspace = true
|
||||
gitbutler-fs.workspace = true
|
||||
gitbutler-diff.workspace = true
|
||||
gitbutler-operating-modes.workspace = true
|
||||
serde = { workspace = true, features = ["std"] }
|
||||
bstr = "1.9.1"
|
||||
diffy = "0.4.0"
|
||||
@ -32,17 +33,13 @@ regex = "1.10"
|
||||
git2-hooks = "0.3"
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
md5 = "0.7.0"
|
||||
futures = "0.3"
|
||||
futures.workspace = true
|
||||
itertools = "0.13"
|
||||
gitbutler-command-context.workspace = true
|
||||
gitbutler-project.workspace = true
|
||||
urlencoding = "2.1.3"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
|
||||
[[test]]
|
||||
name = "virtual"
|
||||
path = "tests/virtual_branches/mod.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell = "1.19"
|
||||
pretty_assertions = "1.4"
|
||||
|
@ -1,36 +1,33 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchId, BranchOwnershipClaims, BranchUpdateRequest};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_operating_modes::assure_open_workspace_mode;
|
||||
use gitbutler_oplog::{
|
||||
entry::{OperationKind, SnapshotDetails},
|
||||
OplogExt, SnapshotExt,
|
||||
};
|
||||
use gitbutler_project::{FetchResult, Project};
|
||||
use gitbutler_reference::{ReferenceName, Refname, RemoteRefname};
|
||||
use gitbutler_repo::{credentials::Helper, RepoActionsExt, RepositoryExt};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::r#virtual as branch;
|
||||
use crate::{
|
||||
base::{
|
||||
get_base_branch_data, set_base_branch, set_target_push_remote, update_base_branch,
|
||||
BaseBranch,
|
||||
},
|
||||
branch_manager::BranchManagerExt,
|
||||
file::RemoteBranchFile,
|
||||
remote::{get_branch_data, list_remote_branches, RemoteBranch, RemoteBranchData},
|
||||
VirtualBranchesExt,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use gitbutler_branch::{
|
||||
BranchOwnershipClaims, {BranchCreateRequest, BranchId, BranchUpdateRequest},
|
||||
};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_oplog::{
|
||||
entry::{OperationKind, SnapshotDetails},
|
||||
OplogExt, SnapshotExt,
|
||||
};
|
||||
use gitbutler_project::{FetchResult, Project};
|
||||
use gitbutler_reference::ReferenceName;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::{credentials::Helper, RepoActionsExt, RepositoryExt};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::r#virtual as branch;
|
||||
|
||||
use crate::file::RemoteBranchFile;
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct VirtualBranchActions;
|
||||
|
||||
impl VirtualBranchActions {
|
||||
pub async fn create_commit(
|
||||
pub fn create_commit(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
@ -38,21 +35,15 @@ impl VirtualBranchActions {
|
||||
ownership: Option<&BranchOwnershipClaims>,
|
||||
run_hooks: bool,
|
||||
) -> Result<git2::Oid> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Creating a commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let snapshot_tree = project_repository
|
||||
.project()
|
||||
.prepare_snapshot(guard.read_permission());
|
||||
let result = branch::commit(
|
||||
&project_repository,
|
||||
branch_id,
|
||||
message,
|
||||
ownership,
|
||||
run_hooks,
|
||||
)
|
||||
.map_err(Into::into);
|
||||
let snapshot_tree = ctx.project().prepare_snapshot(guard.read_permission());
|
||||
let result =
|
||||
branch::commit(&ctx, branch_id, message, ownership, run_hooks).map_err(Into::into);
|
||||
let _ = snapshot_tree.and_then(|snapshot_tree| {
|
||||
project_repository.project().snapshot_commit_creation(
|
||||
ctx.project().snapshot_commit_creation(
|
||||
snapshot_tree,
|
||||
result.as_ref().err(),
|
||||
message.to_owned(),
|
||||
@ -63,34 +54,40 @@ impl VirtualBranchActions {
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn can_apply_remote_branch(
|
||||
pub fn can_apply_remote_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_name: &RemoteRefname,
|
||||
) -> Result<bool> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
branch::is_remote_branch_mergeable(&project_repository, branch_name).map_err(Into::into)
|
||||
let ctx = CommandContext::open(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Testing branch mergability requires open workspace mode")?;
|
||||
branch::is_remote_branch_mergeable(&ctx, branch_name).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn list_virtual_branches(
|
||||
pub fn list_virtual_branches(
|
||||
&self,
|
||||
project: &Project,
|
||||
) -> Result<(Vec<branch::VirtualBranch>, Vec<gitbutler_diff::FileDiff>)> {
|
||||
branch::list_virtual_branches(
|
||||
&open_with_verify(project)?,
|
||||
project.exclusive_worktree_access().write_permission(),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
let ctx = open_with_verify(project)?;
|
||||
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Listing virtual branches requires open workspace mode")?;
|
||||
|
||||
branch::list_virtual_branches(&ctx, project.exclusive_worktree_access().write_permission())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn create_virtual_branch(
|
||||
pub fn create_virtual_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
create: &BranchCreateRequest,
|
||||
) -> Result<BranchId> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Creating a branch requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch_manager = ctx.branch_manager();
|
||||
let branch_id = branch_manager
|
||||
.create_virtual_branch(create, guard.write_permission())?
|
||||
.id;
|
||||
@ -98,81 +95,80 @@ impl VirtualBranchActions {
|
||||
}
|
||||
|
||||
#[instrument(skip(project), err(Debug))]
|
||||
pub async fn get_base_branch_data(project: &Project) -> Result<BaseBranch> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
get_base_branch_data(&project_repository)
|
||||
pub fn get_base_branch_data(project: &Project) -> Result<BaseBranch> {
|
||||
let ctx = CommandContext::open(project)?;
|
||||
get_base_branch_data(&ctx)
|
||||
}
|
||||
|
||||
pub async fn list_remote_commit_files(
|
||||
pub fn list_remote_commit_files(
|
||||
&self,
|
||||
project: &Project,
|
||||
commit_oid: git2::Oid,
|
||||
) -> Result<Vec<RemoteBranchFile>> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
crate::file::list_remote_commit_files(project_repository.repo(), commit_oid)
|
||||
.map_err(Into::into)
|
||||
let ctx = CommandContext::open(project)?;
|
||||
crate::file::list_remote_commit_files(ctx.repository(), commit_oid).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn set_base_branch(
|
||||
pub fn set_base_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
target_branch: &RemoteRefname,
|
||||
) -> Result<BaseBranch> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
let ctx = CommandContext::open(project)?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::SetBaseBranch),
|
||||
guard.write_permission(),
|
||||
);
|
||||
set_base_branch(&project_repository, target_branch)
|
||||
set_base_branch(&ctx, target_branch)
|
||||
}
|
||||
|
||||
pub async fn set_target_push_remote(&self, project: &Project, push_remote: &str) -> Result<()> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
set_target_push_remote(&project_repository, push_remote)
|
||||
pub fn set_target_push_remote(&self, project: &Project, push_remote: &str) -> Result<()> {
|
||||
let ctx = CommandContext::open(project)?;
|
||||
set_target_push_remote(&ctx, push_remote)
|
||||
}
|
||||
|
||||
pub async fn integrate_upstream_commits(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
pub fn integrate_upstream_commits(&self, project: &Project, branch_id: BranchId) -> Result<()> {
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Integrating upstream commits requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::MergeUpstream),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::integrate_upstream_commits(&project_repository, branch_id).map_err(Into::into)
|
||||
branch::integrate_upstream_commits(&ctx, branch_id).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn update_base_branch(&self, project: &Project) -> Result<Vec<ReferenceName>> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
pub fn update_base_branch(&self, project: &Project) -> Result<Vec<ReferenceName>> {
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Updating base branch requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::UpdateWorkspaceBase),
|
||||
guard.write_permission(),
|
||||
);
|
||||
update_base_branch(&project_repository, guard.write_permission()).map_err(Into::into)
|
||||
update_base_branch(&ctx, guard.write_permission()).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn update_virtual_branch(
|
||||
pub fn update_virtual_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_update: BranchUpdateRequest,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Updating a branch requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let snapshot_tree = project_repository
|
||||
.project()
|
||||
.prepare_snapshot(guard.read_permission());
|
||||
let old_branch = project_repository
|
||||
let snapshot_tree = ctx.project().prepare_snapshot(guard.read_permission());
|
||||
let old_branch = ctx
|
||||
.project()
|
||||
.virtual_branches()
|
||||
.get_branch_in_workspace(branch_update.id)?;
|
||||
let result = branch::update_branch(&project_repository, &branch_update);
|
||||
let result = branch::update_branch(&ctx, &branch_update);
|
||||
let _ = snapshot_tree.and_then(|snapshot_tree| {
|
||||
project_repository.project().snapshot_branch_update(
|
||||
ctx.project().snapshot_branch_update(
|
||||
snapshot_tree,
|
||||
&old_branch,
|
||||
&branch_update,
|
||||
@ -183,59 +179,82 @@ impl VirtualBranchActions {
|
||||
result?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn delete_virtual_branch(
|
||||
|
||||
pub fn update_branch_order(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
branch_updates: Vec<BranchUpdateRequest>,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Updating branch order requires open workspace mode")?;
|
||||
for branch_update in branch_updates {
|
||||
let branch = ctx
|
||||
.project()
|
||||
.virtual_branches()
|
||||
.get_branch_in_workspace(branch_update.id)?;
|
||||
if branch_update.order != Some(branch.order) {
|
||||
branch::update_branch(&ctx, &branch_update)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_virtual_branch(&self, project: &Project, branch_id: BranchId) -> Result<()> {
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Deleting a branch order requires open workspace mode")?;
|
||||
let branch_manager = ctx.branch_manager();
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
branch_manager.delete_branch(branch_id, guard.write_permission())
|
||||
}
|
||||
|
||||
pub async fn unapply_ownership(
|
||||
pub fn unapply_ownership(
|
||||
&self,
|
||||
project: &Project,
|
||||
ownership: &BranchOwnershipClaims,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx).context("Unapply a patch requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::DiscardHunk),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::unapply_ownership(&project_repository, ownership, guard.write_permission())
|
||||
.map_err(Into::into)
|
||||
branch::unapply_ownership(&ctx, ownership, guard.write_permission()).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn reset_files(&self, project: &Project, files: &Vec<String>) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
pub fn reset_files(&self, project: &Project, files: &Vec<String>) -> Result<()> {
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Resetting a file requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::DiscardFile),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::reset_files(&project_repository, files).map_err(Into::into)
|
||||
branch::reset_files(&ctx, files).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn amend(
|
||||
pub fn amend(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git2::Oid,
|
||||
ownership: &BranchOwnershipClaims,
|
||||
) -> Result<git2::Oid> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Amending a commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::AmendCommit),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::amend(&project_repository, branch_id, commit_oid, ownership)
|
||||
branch::amend(&ctx, branch_id, commit_oid, ownership)
|
||||
}
|
||||
|
||||
pub async fn move_commit_file(
|
||||
pub fn move_commit_file(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
@ -243,37 +262,33 @@ impl VirtualBranchActions {
|
||||
to_commit_oid: git2::Oid,
|
||||
ownership: &BranchOwnershipClaims,
|
||||
) -> Result<git2::Oid> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Amending a commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::MoveCommitFile),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::move_commit_file(
|
||||
&project_repository,
|
||||
branch_id,
|
||||
from_commit_oid,
|
||||
to_commit_oid,
|
||||
ownership,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
branch::move_commit_file(&ctx, branch_id, from_commit_oid, to_commit_oid, ownership)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn undo_commit(
|
||||
pub fn undo_commit(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git2::Oid,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Undoing a commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let snapshot_tree = project_repository
|
||||
.project()
|
||||
.prepare_snapshot(guard.read_permission());
|
||||
let snapshot_tree = ctx.project().prepare_snapshot(guard.read_permission());
|
||||
let result: Result<()> =
|
||||
branch::undo_commit(&project_repository, branch_id, commit_oid).map_err(Into::into);
|
||||
branch::undo_commit(&ctx, branch_id, commit_oid).map_err(Into::into);
|
||||
let _ = snapshot_tree.and_then(|snapshot_tree| {
|
||||
project_repository.project().snapshot_commit_undo(
|
||||
ctx.project().snapshot_commit_undo(
|
||||
snapshot_tree,
|
||||
result.as_ref(),
|
||||
commit_oid,
|
||||
@ -283,70 +298,74 @@ impl VirtualBranchActions {
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn insert_blank_commit(
|
||||
pub fn insert_blank_commit(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git2::Oid,
|
||||
offset: i32,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Inserting a blank commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::InsertBlankCommit),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::insert_blank_commit(&project_repository, branch_id, commit_oid, offset)
|
||||
.map_err(Into::into)
|
||||
branch::insert_blank_commit(&ctx, branch_id, commit_oid, offset).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn reorder_commit(
|
||||
pub fn reorder_commit(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git2::Oid,
|
||||
offset: i32,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Reordering a commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::ReorderCommit),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::reorder_commit(&project_repository, branch_id, commit_oid, offset)
|
||||
.map_err(Into::into)
|
||||
branch::reorder_commit(&ctx, branch_id, commit_oid, offset).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn reset_virtual_branch(
|
||||
pub fn reset_virtual_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
target_commit_oid: git2::Oid,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Resetting a branch requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::UndoCommit),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::reset_branch(&project_repository, branch_id, target_commit_oid).map_err(Into::into)
|
||||
branch::reset_branch(&ctx, branch_id, target_commit_oid).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn convert_to_real_branch(
|
||||
pub fn convert_to_real_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
) -> Result<ReferenceName> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Converting branch to a real branch requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let snapshot_tree = project_repository
|
||||
.project()
|
||||
.prepare_snapshot(guard.read_permission());
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let snapshot_tree = ctx.project().prepare_snapshot(guard.read_permission());
|
||||
let branch_manager = ctx.branch_manager();
|
||||
let result = branch_manager.convert_to_real_branch(branch_id, guard.write_permission());
|
||||
|
||||
let _ = snapshot_tree.and_then(|snapshot_tree| {
|
||||
project_repository.project().snapshot_branch_unapplied(
|
||||
ctx.project().snapshot_branch_unapplied(
|
||||
snapshot_tree,
|
||||
result.as_ref(),
|
||||
guard.write_permission(),
|
||||
@ -356,7 +375,7 @@ impl VirtualBranchActions {
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn push_virtual_branch(
|
||||
pub fn push_virtual_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
@ -364,70 +383,74 @@ impl VirtualBranchActions {
|
||||
askpass: Option<Option<BranchId>>,
|
||||
) -> Result<()> {
|
||||
let helper = Helper::default();
|
||||
let project_repository = open_with_verify(project)?;
|
||||
branch::push(&project_repository, branch_id, with_force, &helper, askpass)
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Pushing a branch requires open workspace mode")?;
|
||||
branch::push(&ctx, branch_id, with_force, &helper, askpass)
|
||||
}
|
||||
|
||||
pub async fn list_remote_branches(project: Project) -> Result<Vec<RemoteBranch>> {
|
||||
let project_repository = ProjectRepository::open(&project)?;
|
||||
list_remote_branches(&project_repository)
|
||||
pub fn list_remote_branches(project: Project) -> Result<Vec<RemoteBranch>> {
|
||||
let ctx = CommandContext::open(&project)?;
|
||||
list_remote_branches(&ctx)
|
||||
}
|
||||
|
||||
pub async fn get_remote_branch_data(
|
||||
pub fn get_remote_branch_data(
|
||||
&self,
|
||||
project: &Project,
|
||||
refname: &Refname,
|
||||
) -> Result<RemoteBranchData> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
get_branch_data(&project_repository, refname)
|
||||
let ctx = CommandContext::open(project)?;
|
||||
get_branch_data(&ctx, refname)
|
||||
}
|
||||
|
||||
pub async fn squash(
|
||||
pub fn squash(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git2::Oid,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Squashing a commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::SquashCommit),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::squash(&project_repository, branch_id, commit_oid).map_err(Into::into)
|
||||
branch::squash(&ctx, branch_id, commit_oid).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn update_commit_message(
|
||||
pub fn update_commit_message(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch_id: BranchId,
|
||||
commit_oid: git2::Oid,
|
||||
message: &str,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Updating a commit message requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::UpdateCommitMessage),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::update_commit_message(&project_repository, branch_id, commit_oid, message)
|
||||
.map_err(Into::into)
|
||||
branch::update_commit_message(&ctx, branch_id, commit_oid, message).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn fetch_from_remotes(
|
||||
pub fn fetch_from_remotes(
|
||||
&self,
|
||||
project: &Project,
|
||||
askpass: Option<String>,
|
||||
) -> Result<FetchResult> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
let ctx = CommandContext::open(project)?;
|
||||
|
||||
let helper = Helper::default();
|
||||
let remotes = project_repository.repo().remotes_as_string()?;
|
||||
let remotes = ctx.repository().remotes_as_string()?;
|
||||
let fetch_errors: Vec<_> = remotes
|
||||
.iter()
|
||||
.filter_map(|remote| {
|
||||
project_repository
|
||||
.fetch(remote, &helper, askpass.clone())
|
||||
ctx.fetch(remote, &helper, askpass.clone())
|
||||
.err()
|
||||
.map(|err| err.to_string())
|
||||
})
|
||||
@ -446,29 +469,32 @@ impl VirtualBranchActions {
|
||||
Ok(project_data_last_fetched)
|
||||
}
|
||||
|
||||
pub async fn move_commit(
|
||||
pub fn move_commit(
|
||||
&self,
|
||||
project: &Project,
|
||||
target_branch_id: BranchId,
|
||||
commit_oid: git2::Oid,
|
||||
) -> Result<()> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx).context("Moving a commit requires open workspace mode")?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
let _ = project_repository.project().create_snapshot(
|
||||
let _ = ctx.project().create_snapshot(
|
||||
SnapshotDetails::new(OperationKind::MoveCommit),
|
||||
guard.write_permission(),
|
||||
);
|
||||
branch::move_commit(&project_repository, target_branch_id, commit_oid).map_err(Into::into)
|
||||
branch::move_commit(&ctx, target_branch_id, commit_oid).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn create_virtual_branch_from_branch(
|
||||
pub fn create_virtual_branch_from_branch(
|
||||
&self,
|
||||
project: &Project,
|
||||
branch: &Refname,
|
||||
remote: Option<RemoteRefname>,
|
||||
) -> Result<BranchId> {
|
||||
let project_repository = open_with_verify(project)?;
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let ctx = open_with_verify(project)?;
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Creating a virtual branch from a branch open workspace mode")?;
|
||||
let branch_manager = ctx.branch_manager();
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
branch_manager
|
||||
.create_virtual_branch_from_branch(branch, remote, guard.write_permission())
|
||||
@ -476,9 +502,9 @@ impl VirtualBranchActions {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_with_verify(project: &Project) -> Result<ProjectRepository> {
|
||||
let project_repository = ProjectRepository::open(project)?;
|
||||
fn open_with_verify(project: &Project) -> Result<CommandContext> {
|
||||
let ctx = CommandContext::open(project)?;
|
||||
let mut guard = project.exclusive_worktree_access();
|
||||
crate::integration::verify_branch(&project_repository, guard.write_permission())?;
|
||||
Ok(project_repository)
|
||||
crate::integration::verify_branch(&ctx, guard.write_permission())?;
|
||||
Ok(ctx)
|
||||
}
|
||||
|
@ -2,29 +2,26 @@ use std::{path::Path, time};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use git2::Index;
|
||||
use gitbutler_branch::Branch;
|
||||
use gitbutler_branch::BranchOwnershipClaims;
|
||||
use gitbutler_branch::Target;
|
||||
use gitbutler_branch::VirtualBranchesHandle;
|
||||
use gitbutler_branch::{self, BranchId};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_project::FetchResult;
|
||||
use gitbutler_reference::ReferenceName;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
|
||||
use gitbutler_branch::{
|
||||
self, Branch, BranchId, BranchOwnershipClaims, Target, VirtualBranchesHandle,
|
||||
GITBUTLER_INTEGRATION_REFERENCE,
|
||||
};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_project::{access::WorktreeWritePermission, FetchResult};
|
||||
use gitbutler_reference::{ReferenceName, Refname, RemoteRefname};
|
||||
use gitbutler_repo::{rebase::cherry_rebase, LogUntil, RepoActionsExt, RepositoryExt};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::branch_manager::BranchManagerExt;
|
||||
use crate::conflicts::RepoConflictsExt;
|
||||
use crate::hunk::VirtualBranchHunk;
|
||||
use crate::integration::update_gitbutler_integration;
|
||||
use crate::remote::{commit_to_remote_commit, RemoteCommit};
|
||||
use crate::status::get_applied_status;
|
||||
use crate::VirtualBranchesExt;
|
||||
use gitbutler_branch::GITBUTLER_INTEGRATION_REFERENCE;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_repo::rebase::cherry_rebase;
|
||||
use crate::{
|
||||
branch_manager::BranchManagerExt,
|
||||
conflicts::RepoConflictsExt,
|
||||
hunk::VirtualBranchHunk,
|
||||
integration::update_gitbutler_integration,
|
||||
remote::{commit_to_remote_commit, RemoteCommit},
|
||||
status::get_applied_status,
|
||||
VirtualBranchesExt,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -44,18 +41,15 @@ pub struct BaseBranch {
|
||||
pub last_fetched_ms: Option<u128>,
|
||||
}
|
||||
|
||||
pub(crate) fn get_base_branch_data(project_repository: &ProjectRepository) -> Result<BaseBranch> {
|
||||
let target = default_target(&project_repository.project().gb_dir())?;
|
||||
let base = target_to_base_branch(project_repository, &target)?;
|
||||
pub(crate) fn get_base_branch_data(ctx: &CommandContext) -> Result<BaseBranch> {
|
||||
let target = default_target(&ctx.project().gb_dir())?;
|
||||
let base = target_to_base_branch(ctx, &target)?;
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
fn go_back_to_integration(
|
||||
project_repository: &ProjectRepository,
|
||||
default_target: &Target,
|
||||
) -> Result<BaseBranch> {
|
||||
let statuses = project_repository
|
||||
.repo()
|
||||
fn go_back_to_integration(ctx: &CommandContext, default_target: &Target) -> Result<BaseBranch> {
|
||||
let statuses = ctx
|
||||
.repository()
|
||||
.statuses(Some(
|
||||
git2::StatusOptions::new()
|
||||
.show(git2::StatusShow::IndexAndWorkdir)
|
||||
@ -66,13 +60,13 @@ fn go_back_to_integration(
|
||||
return Err(anyhow!("current HEAD is dirty")).context(Marker::ProjectConflict);
|
||||
}
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
let virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to read virtual branches")?;
|
||||
|
||||
let target_commit = project_repository
|
||||
.repo()
|
||||
let target_commit = ctx
|
||||
.repository()
|
||||
.find_commit(default_target.sha)
|
||||
.context("failed to find target commit")?;
|
||||
|
||||
@ -84,48 +78,47 @@ fn go_back_to_integration(
|
||||
.context("failed to get base tree from commit")?;
|
||||
for branch in &virtual_branches {
|
||||
// merge this branches tree with our tree
|
||||
let branch_head = project_repository
|
||||
.repo()
|
||||
let branch_head = ctx
|
||||
.repository()
|
||||
.find_commit(branch.head)
|
||||
.context("failed to find branch head")?;
|
||||
let branch_tree = branch_head
|
||||
.tree()
|
||||
.context("failed to get branch head tree")?;
|
||||
let mut result = project_repository
|
||||
.repo()
|
||||
let mut result = ctx
|
||||
.repository()
|
||||
.merge_trees(&base_tree, &final_tree, &branch_tree, None)
|
||||
.context("failed to merge")?;
|
||||
let final_tree_oid = result
|
||||
.write_tree_to(project_repository.repo())
|
||||
.write_tree_to(ctx.repository())
|
||||
.context("failed to write tree")?;
|
||||
final_tree = project_repository
|
||||
.repo()
|
||||
final_tree = ctx
|
||||
.repository()
|
||||
.find_tree(final_tree_oid)
|
||||
.context("failed to find written tree")?;
|
||||
}
|
||||
|
||||
project_repository
|
||||
.repo()
|
||||
ctx.repository()
|
||||
.checkout_tree_builder(&final_tree)
|
||||
.force()
|
||||
.checkout()
|
||||
.context("failed to checkout tree")?;
|
||||
|
||||
let base = target_to_base_branch(project_repository, default_target)?;
|
||||
update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
let base = target_to_base_branch(ctx, default_target)?;
|
||||
update_gitbutler_integration(&vb_state, ctx)?;
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
pub(crate) fn set_base_branch(
|
||||
project_repository: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
target_branch_ref: &RemoteRefname,
|
||||
) -> Result<BaseBranch> {
|
||||
let repo = project_repository.repo();
|
||||
let repo = ctx.repository();
|
||||
|
||||
// if target exists, and it is the same as the requested branch, we should go back
|
||||
if let Ok(target) = default_target(&project_repository.project().gb_dir()) {
|
||||
if let Ok(target) = default_target(&ctx.project().gb_dir()) {
|
||||
if target.branch.eq(target_branch_ref) {
|
||||
return go_back_to_integration(project_repository, &target);
|
||||
return go_back_to_integration(ctx, &target);
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +150,7 @@ pub(crate) fn set_base_branch(
|
||||
.peel_to_commit()
|
||||
.context("Failed to peel HEAD reference to commit")?;
|
||||
|
||||
// calculate the commit as the merge-base between HEAD in project_repository and this target commit
|
||||
// calculate the commit as the merge-base between HEAD in ctx and this target commit
|
||||
let target_commit_oid = repo
|
||||
.merge_base(current_head_commit.id(), target_branch_head.id())
|
||||
.context(format!(
|
||||
@ -173,7 +166,7 @@ pub(crate) fn set_base_branch(
|
||||
push_remote_name: None,
|
||||
};
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
vb_state.set_default_target(target.clone())?;
|
||||
|
||||
// TODO: make sure this is a real branch
|
||||
@ -247,14 +240,14 @@ pub(crate) fn set_base_branch(
|
||||
updated_timestamp_ms: now_ms,
|
||||
head: current_head_commit.id(),
|
||||
tree: gitbutler_diff::write::hunks_onto_commit(
|
||||
project_repository,
|
||||
ctx,
|
||||
current_head_commit.id(),
|
||||
gitbutler_diff::diff_files_into_hunks(wd_diff),
|
||||
)?,
|
||||
ownership,
|
||||
order: 0,
|
||||
selected_for_changes: None,
|
||||
allow_rebasing: project_repository.project().ok_with_force_push.into(),
|
||||
allow_rebasing: ctx.project().ok_with_force_push.into(),
|
||||
applied: true,
|
||||
in_workspace: true,
|
||||
not_in_workspace_wip_change_id: None,
|
||||
@ -264,38 +257,35 @@ pub(crate) fn set_base_branch(
|
||||
}
|
||||
}
|
||||
|
||||
set_exclude_decoration(project_repository)?;
|
||||
set_exclude_decoration(ctx)?;
|
||||
|
||||
update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
update_gitbutler_integration(&vb_state, ctx)?;
|
||||
|
||||
let base = target_to_base_branch(project_repository, &target)?;
|
||||
let base = target_to_base_branch(ctx, &target)?;
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
pub(crate) fn set_target_push_remote(
|
||||
project_repository: &ProjectRepository,
|
||||
push_remote_name: &str,
|
||||
) -> Result<()> {
|
||||
let remote = project_repository
|
||||
.repo()
|
||||
pub(crate) fn set_target_push_remote(ctx: &CommandContext, push_remote_name: &str) -> Result<()> {
|
||||
let remote = ctx
|
||||
.repository()
|
||||
.find_remote(push_remote_name)
|
||||
.context(format!("failed to find remote {}", push_remote_name))?;
|
||||
|
||||
// if target exists, and it is the same as the requested branch, we should go back
|
||||
let mut target = default_target(&project_repository.project().gb_dir())?;
|
||||
let mut target = default_target(&ctx.project().gb_dir())?;
|
||||
target.push_remote_name = remote
|
||||
.name()
|
||||
.context("failed to get remote name")?
|
||||
.to_string()
|
||||
.into();
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
vb_state.set_default_target(target)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_exclude_decoration(project_repository: &ProjectRepository) -> Result<()> {
|
||||
let repo = project_repository.repo();
|
||||
fn set_exclude_decoration(ctx: &CommandContext) -> Result<()> {
|
||||
let repo = ctx.repository();
|
||||
let mut config = repo.config()?;
|
||||
config
|
||||
.set_multivar("log.excludeDecoration", "refs/gitbutler", "refs/gitbutler")
|
||||
@ -330,14 +320,14 @@ fn _print_tree(repo: &git2::Repository, tree: &git2::Tree) -> Result<()> {
|
||||
// merge the target branch into our current working directory
|
||||
// update the target sha
|
||||
pub(crate) fn update_base_branch(
|
||||
project_repository: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
perm: &mut WorktreeWritePermission,
|
||||
) -> anyhow::Result<Vec<ReferenceName>> {
|
||||
project_repository.assure_resolved()?;
|
||||
ctx.assure_resolved()?;
|
||||
|
||||
// look up the target and see if there is a new oid
|
||||
let target = default_target(&project_repository.project().gb_dir())?;
|
||||
let repo = project_repository.repo();
|
||||
let target = default_target(&ctx.project().gb_dir())?;
|
||||
let repo = ctx.repository();
|
||||
let target_branch = repo
|
||||
.find_branch_by_refname(&target.branch.clone().into())
|
||||
.context(format!("failed to find branch {}", target.branch))?;
|
||||
@ -363,10 +353,10 @@ pub(crate) fn update_base_branch(
|
||||
target.sha
|
||||
))?;
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
|
||||
// try to update every branch
|
||||
let updated_vbranches = get_applied_status(project_repository, None)?
|
||||
let updated_vbranches = get_applied_status(ctx, None)?
|
||||
.branches
|
||||
.into_iter()
|
||||
.map(|(branch, _)| branch)
|
||||
@ -393,16 +383,13 @@ pub(crate) fn update_base_branch(
|
||||
branch.upstream = None;
|
||||
branch.upstream_head = None;
|
||||
|
||||
let non_commited_files = gitbutler_diff::trees(
|
||||
project_repository.repo(),
|
||||
&branch_head_tree,
|
||||
&branch_tree,
|
||||
)?;
|
||||
let non_commited_files =
|
||||
gitbutler_diff::trees(ctx.repository(), &branch_head_tree, &branch_tree)?;
|
||||
if non_commited_files.is_empty() {
|
||||
// if there are no commited files, then the branch is fully merged
|
||||
// and we can delete it.
|
||||
vb_state.mark_as_not_in_workspace(branch.id)?;
|
||||
project_repository.delete_branch_reference(&branch)?;
|
||||
ctx.delete_branch_reference(&branch)?;
|
||||
Ok(None)
|
||||
} else {
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
@ -421,7 +408,7 @@ pub(crate) fn update_base_branch(
|
||||
|
||||
if branch_tree_merge_index.has_conflicts() {
|
||||
// branch tree conflicts with new target, unapply branch for now. we'll handle it later, when user applies it back.
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch_manager = ctx.branch_manager();
|
||||
let unapplied_real_branch =
|
||||
branch_manager.convert_to_real_branch(branch.id, perm)?;
|
||||
|
||||
@ -431,7 +418,7 @@ pub(crate) fn update_base_branch(
|
||||
}
|
||||
|
||||
let branch_merge_index_tree_oid =
|
||||
branch_tree_merge_index.write_tree_to(project_repository.repo())?;
|
||||
branch_tree_merge_index.write_tree_to(ctx.repository())?;
|
||||
|
||||
if branch_merge_index_tree_oid == new_target_tree.id() {
|
||||
return result_integrated_detected(branch);
|
||||
@ -455,7 +442,7 @@ pub(crate) fn update_base_branch(
|
||||
if branch_head_merge_index.has_conflicts() {
|
||||
// branch commits conflict with new target, make sure the branch is
|
||||
// unapplied. conflicts witll be dealt with when applying it back.
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch_manager = ctx.branch_manager();
|
||||
let unapplied_real_branch =
|
||||
branch_manager.convert_to_real_branch(branch.id, perm)?;
|
||||
unapplied_branch_names.push(unapplied_real_branch);
|
||||
@ -465,7 +452,7 @@ pub(crate) fn update_base_branch(
|
||||
|
||||
// branch commits do not conflict with new target, so lets merge them
|
||||
let branch_head_merge_tree_oid = branch_head_merge_index
|
||||
.write_tree_to(project_repository.repo())
|
||||
.write_tree_to(ctx.repository())
|
||||
.context(format!(
|
||||
"failed to write head merge index for {}",
|
||||
branch.id
|
||||
@ -480,7 +467,7 @@ pub(crate) fn update_base_branch(
|
||||
.find_tree(branch_head_merge_tree_oid)
|
||||
.context("failed to find tree")?;
|
||||
|
||||
let new_target_head = project_repository
|
||||
let new_target_head = ctx
|
||||
.commit(
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
@ -507,7 +494,7 @@ pub(crate) fn update_base_branch(
|
||||
|
||||
// branch was not pushed to upstream yet. attempt a rebase,
|
||||
let rebased_head_oid = cherry_rebase(
|
||||
project_repository,
|
||||
ctx,
|
||||
new_target_commit.id(),
|
||||
new_target_commit.id(),
|
||||
branch.head,
|
||||
@ -561,15 +548,12 @@ pub(crate) fn update_base_branch(
|
||||
})?;
|
||||
|
||||
// Rewriting the integration commit is necessary after changing target sha.
|
||||
crate::integration::update_gitbutler_integration(&vb_state, project_repository)?;
|
||||
crate::integration::update_gitbutler_integration(&vb_state, ctx)?;
|
||||
Ok(unapplied_branch_names)
|
||||
}
|
||||
|
||||
pub(crate) fn target_to_base_branch(
|
||||
project_repository: &ProjectRepository,
|
||||
target: &Target,
|
||||
) -> Result<BaseBranch> {
|
||||
let repo = project_repository.repo();
|
||||
pub(crate) fn target_to_base_branch(ctx: &CommandContext, target: &Target) -> Result<BaseBranch> {
|
||||
let repo = ctx.repository();
|
||||
let branch = repo
|
||||
.find_branch_by_refname(&target.branch.clone().into())?
|
||||
.ok_or(anyhow!("failed to get branch"))?;
|
||||
@ -577,7 +561,7 @@ pub(crate) fn target_to_base_branch(
|
||||
let oid = commit.id();
|
||||
|
||||
// gather a list of commits between oid and target.sha
|
||||
let upstream_commits = project_repository
|
||||
let upstream_commits = ctx
|
||||
.log(oid, LogUntil::Commit(target.sha))
|
||||
.context("failed to get upstream commits")?
|
||||
.iter()
|
||||
@ -585,7 +569,7 @@ pub(crate) fn target_to_base_branch(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// get some recent commits
|
||||
let recent_commits = project_repository
|
||||
let recent_commits = ctx
|
||||
.log(target.sha, LogUntil::Take(20))
|
||||
.context("failed to get recent commits")?
|
||||
.iter()
|
||||
@ -615,7 +599,7 @@ pub(crate) fn target_to_base_branch(
|
||||
behind: upstream_commits.len(),
|
||||
upstream_commits,
|
||||
recent_commits,
|
||||
last_fetched_ms: project_repository
|
||||
last_fetched_ms: ctx
|
||||
.project()
|
||||
.project_data_last_fetch
|
||||
.as_ref()
|
||||
|
@ -1,28 +1,63 @@
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::vec;
|
||||
use std::{
|
||||
cmp::max,
|
||||
collections::{HashMap, HashSet},
|
||||
vec,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use bstr::{BString, ByteSlice};
|
||||
use gitbutler_branch::Branch as GitButlerBranch;
|
||||
use gitbutler_branch::BranchId;
|
||||
use gitbutler_branch::ReferenceExt;
|
||||
use gitbutler_branch::Target;
|
||||
use gitbutler_branch::VirtualBranchesHandle;
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
|
||||
use gitbutler_branch::{
|
||||
Branch as GitButlerBranch, BranchId, ReferenceExt, Target, VirtualBranchesHandle,
|
||||
};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_reference::normalize_branch_name;
|
||||
use serde::Serialize;
|
||||
use gitbutler_repo::RepoActionsExt;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{VirtualBranch, VirtualBranchesExt};
|
||||
use crate::VirtualBranchesExt;
|
||||
|
||||
/// Returns a list of branches associated with this project.
|
||||
// TODO: Implement pagination for this thing
|
||||
pub fn list_branches(ctx: &ProjectRepository) -> Result<Vec<BranchListing>> {
|
||||
pub fn list_branches(
|
||||
ctx: &CommandContext,
|
||||
filter: Option<BranchListingFilter>,
|
||||
) -> Result<Vec<BranchListing>> {
|
||||
let vb_handle = ctx.project().virtual_branches();
|
||||
let branches = ctx.repo().branches(None)?;
|
||||
// The definition of "own_branch" is based if the current user made the first commit on the branch
|
||||
// However, because getting that info is both expensive and also we cant filter ahead of time,
|
||||
// here we assume that all of the "own_branches" will be local.
|
||||
let branch_filter = filter
|
||||
.as_ref()
|
||||
.and_then(|filter| match filter.own_branches {
|
||||
Some(true) => Some(git2::BranchType::Local),
|
||||
_ => None,
|
||||
});
|
||||
let mut git_branches: Vec<GroupBranch> = vec![];
|
||||
for result in ctx.repository().branches(branch_filter)? {
|
||||
match result {
|
||||
Ok((branch, branch_type)) => match branch_type {
|
||||
git2::BranchType::Local => {
|
||||
if branch_filter
|
||||
.map(|branch_type| branch_type == git2::BranchType::Local)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// If we had an "own_branch" filter, we skipped getting the remote branches, however we still want the remote
|
||||
// tracking branches for the ones that are local
|
||||
if let Ok(upstream) = branch.upstream() {
|
||||
git_branches.push(GroupBranch::Remote(upstream));
|
||||
}
|
||||
}
|
||||
git_branches.push(GroupBranch::Local(branch));
|
||||
}
|
||||
git2::BranchType::Remote => {
|
||||
git_branches.push(GroupBranch::Remote(branch));
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// virtual branches from the application state
|
||||
let virtual_branches = ctx
|
||||
@ -31,34 +66,45 @@ pub fn list_branches(ctx: &ProjectRepository) -> Result<Vec<BranchListing>> {
|
||||
.list_all_branches()?
|
||||
.into_iter();
|
||||
|
||||
combine_branches(branches, virtual_branches, ctx.repo(), &vb_handle)
|
||||
let branches = combine_branches(git_branches, virtual_branches, ctx, &vb_handle)?;
|
||||
// Apply the filter
|
||||
let branches: Vec<BranchListing> = branches
|
||||
.into_iter()
|
||||
.filter(|branch| matches_all(branch, &filter))
|
||||
.sorted_by(|a, b| b.updated_at.cmp(&a.updated_at))
|
||||
.collect();
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
fn matches_all(branch: &BranchListing, filter: &Option<BranchListingFilter>) -> bool {
|
||||
if let Some(filter) = filter {
|
||||
let mut conditions: Vec<bool> = vec![];
|
||||
if let Some(applied) = filter.applied {
|
||||
if let Some(vb) = branch.virtual_branch.as_ref() {
|
||||
conditions.push(applied == vb.in_workspace);
|
||||
} else {
|
||||
conditions.push(!applied);
|
||||
}
|
||||
}
|
||||
if let Some(own) = filter.own_branches {
|
||||
conditions.push(own == branch.own_branch);
|
||||
}
|
||||
return conditions.iter().all(|&x| x);
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_branches(
|
||||
branches: git2::Branches,
|
||||
mut group_branches: Vec<GroupBranch>,
|
||||
virtual_branches: impl Iterator<Item = GitButlerBranch>,
|
||||
repo: &git2::Repository,
|
||||
ctx: &CommandContext,
|
||||
vb_handle: &VirtualBranchesHandle,
|
||||
) -> Result<Vec<BranchListing>> {
|
||||
let mut group_branches: Vec<GroupBranch> = vec![];
|
||||
let repo = ctx.repository();
|
||||
for branch in virtual_branches {
|
||||
group_branches.push(GroupBranch::Virtual(branch));
|
||||
}
|
||||
for result in branches {
|
||||
match result {
|
||||
Ok((branch, branch_type)) => match branch_type {
|
||||
git2::BranchType::Local => {
|
||||
group_branches.push(GroupBranch::Local(branch));
|
||||
}
|
||||
git2::BranchType::Remote => {
|
||||
group_branches.push(GroupBranch::Remote(branch));
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let remotes = repo.remotes()?;
|
||||
let target_branch = vb_handle.get_default_target().ok();
|
||||
// Group branches by identity
|
||||
@ -75,11 +121,7 @@ fn combine_branches(
|
||||
groups.insert(identity, vec![branch]);
|
||||
}
|
||||
}
|
||||
let config = repo.config()?;
|
||||
let local_author = Author {
|
||||
name: config.get_string("user.name").ok(),
|
||||
email: config.get_string("user.email").ok(),
|
||||
};
|
||||
let (local_author, _committer) = ctx.signatures()?;
|
||||
|
||||
// Convert to Branch entries for the API response, filtering out any errors
|
||||
let branches: Vec<BranchListing> = groups
|
||||
@ -109,7 +151,7 @@ fn branch_group_to_branch(
|
||||
identity: Option<String>,
|
||||
group_branches: Vec<&GroupBranch>,
|
||||
repo: &git2::Repository,
|
||||
local_author: &Author,
|
||||
local_author: &git2::Signature,
|
||||
) -> Result<BranchListing> {
|
||||
let virtual_branch = group_branches
|
||||
.iter()
|
||||
@ -169,62 +211,51 @@ fn branch_group_to_branch(
|
||||
.map(|vb| normalize_branch_name(&vb.name))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let last_modified_ms = max(
|
||||
(repo.find_commit(head)?.time().seconds() * 1000) as u128,
|
||||
virtual_branch.map_or(0, |x| x.updated_timestamp_ms),
|
||||
);
|
||||
let repo_head = repo.head()?.peel_to_commit()?;
|
||||
// If no merge base can be found, return with zero stats
|
||||
let branch = if let Ok(base) = repo.merge_base(repo_head.id(), head) {
|
||||
let base_tree = repo.find_commit(base)?.tree()?;
|
||||
let head_tree = repo.find_commit(head)?.tree()?;
|
||||
let diff_stats = repo
|
||||
.diff_tree_to_tree(Some(&base_tree), Some(&head_tree), None)?
|
||||
.stats()?;
|
||||
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.push(head)?;
|
||||
revwalk.hide(base)?;
|
||||
let mut commits = Vec::new();
|
||||
let mut authors = HashSet::new();
|
||||
let mut last_commit_time_ms = i64::MIN;
|
||||
for oid in revwalk {
|
||||
let commit = repo.find_commit(oid?)?;
|
||||
last_commit_time_ms = max(last_commit_time_ms, commit.time().seconds() * 1000);
|
||||
authors.insert(commit.author().into());
|
||||
commits.push(commit);
|
||||
}
|
||||
|
||||
let own_branch = commits
|
||||
.last()
|
||||
.map_or(false, |commit| local_author == &commit.author().into());
|
||||
|
||||
let last_modified_ms = max(
|
||||
last_commit_time_ms as u128,
|
||||
virtual_branch.map_or(0, |x| x.updated_timestamp_ms),
|
||||
);
|
||||
// If there are no commits (i.e. virtual branch only) it is considered the users own
|
||||
let own_branch = commits.is_empty()
|
||||
|| commits.iter().any(|commit| {
|
||||
let commit_author = commit.author();
|
||||
local_author.name_bytes() == commit_author.name_bytes()
|
||||
&& local_author.email_bytes() == commit_author.email_bytes()
|
||||
});
|
||||
|
||||
BranchListing {
|
||||
name: identity,
|
||||
remotes,
|
||||
virtual_branch: virtual_branch_reference,
|
||||
lines_added: diff_stats.insertions(),
|
||||
lines_removed: diff_stats.deletions(),
|
||||
number_of_files: diff_stats.files_changed(),
|
||||
number_of_commits: commits.len(),
|
||||
updated_at: last_modified_ms,
|
||||
authors: authors.into_iter().collect(),
|
||||
own_branch,
|
||||
head,
|
||||
}
|
||||
} else {
|
||||
let last_modified_ms = (repo.find_commit(head)?.time().seconds() * 1000) as u128;
|
||||
BranchListing {
|
||||
name: identity,
|
||||
remotes,
|
||||
virtual_branch: virtual_branch_reference,
|
||||
lines_added: 0,
|
||||
lines_removed: 0,
|
||||
number_of_files: 0,
|
||||
number_of_commits: 0,
|
||||
updated_at: last_modified_ms,
|
||||
authors: Vec::new(),
|
||||
own_branch: false,
|
||||
head,
|
||||
}
|
||||
};
|
||||
Ok(branch)
|
||||
@ -278,6 +309,18 @@ fn should_list_git_branch(identity: &Option<String>, target: &Option<Target>) ->
|
||||
true
|
||||
}
|
||||
|
||||
/// A filter that can be applied to the branch listing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BranchListingFilter {
|
||||
/// If the value is true, the listing will only include branches that have the same author as the current user.
|
||||
/// If the value is false, the listing will include only branches that are not created by the user.
|
||||
pub own_branches: Option<bool>,
|
||||
/// If the value is true, the listing will only include branches that are applied in the workspace.
|
||||
/// If the value is false, the listing will only include branches that are not applied in the workspace.
|
||||
pub applied: Option<bool>,
|
||||
}
|
||||
|
||||
/// Represents a branch that exists for the repository
|
||||
/// This also combines the concept of a remote, local and virtual branch in order to provide a unified interface for the UI
|
||||
/// Branch entry is not meant to contain all of the data a branch can have (e.g. full commit history, all files and diffs, etc.).
|
||||
@ -294,23 +337,6 @@ pub struct BranchListing {
|
||||
pub remotes: Vec<BString>,
|
||||
/// The branch may or may not have a virtual branch associated with it
|
||||
pub virtual_branch: Option<VirtualBranchReference>,
|
||||
/// The number of lines added within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple).
|
||||
/// If this branch has a virutal branch, lines_added does NOT include the uncommitted lines.
|
||||
pub lines_added: usize,
|
||||
/// The number of lines removed within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
/// If this branch has a virutal branch, lines_removed does NOT include the uncommitted lines.
|
||||
pub lines_removed: usize,
|
||||
/// The number of files that were modified within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number files modified,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
pub number_of_files: usize,
|
||||
/// The number of commits associated with a branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of commits,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
@ -322,9 +348,17 @@ pub struct BranchListing {
|
||||
/// A list of authors that have contributes commits to this branch.
|
||||
/// In the case of multiple remote tracking branches, it takes the full list of unique authors.
|
||||
pub authors: Vec<Author>,
|
||||
/// Determines if the branch is considered one created by the user
|
||||
/// A branch is considered created by the user if they were the author of the first commit in the branch.
|
||||
/// Determines if the current user is involved with this branch.
|
||||
/// Returns true if the author has created a commit on this branch
|
||||
/// If it is a virtual branch, if it has zero commits it is also considered as the user's branch
|
||||
pub own_branch: bool,
|
||||
/// The head of interest for the branch group, used for calculating branch statistics.
|
||||
/// If there is a virtual branch, a local branch and remote branches, the head is determined in the following order:
|
||||
/// 1. The head of the virtual branch
|
||||
/// 2. The head of the local branch
|
||||
/// 3. The head of the first remote branch
|
||||
#[serde(skip)]
|
||||
head: git2::Oid,
|
||||
}
|
||||
|
||||
/// Represents a "commit author" or "signature", based on the data from ther git history
|
||||
@ -356,21 +390,62 @@ pub struct VirtualBranchReference {
|
||||
pub in_workspace: bool,
|
||||
}
|
||||
|
||||
/// Takes a list of branch names (the given name, as returned by `BranchListing`) and returns
|
||||
/// a list of enriched branch data in the form of `BranchData`.
|
||||
pub fn get_branch_listing_details(
|
||||
ctx: &CommandContext,
|
||||
branch_names: Vec<String>,
|
||||
) -> Result<Vec<BranchListingDetails>> {
|
||||
let repo = ctx.repository();
|
||||
// Can we do this in a more efficient way?
|
||||
let branches = list_branches(ctx, None)?
|
||||
.into_iter()
|
||||
.filter(|branch| branch_names.contains(&branch.name))
|
||||
.collect::<Vec<_>>();
|
||||
let repo_head = repo.head()?.peel_to_commit()?;
|
||||
let mut enriched_branches = Vec::new();
|
||||
for branch in branches {
|
||||
if let Ok(base) = repo.merge_base(repo_head.id(), branch.head) {
|
||||
let base_tree = repo.find_commit(base)?.tree()?;
|
||||
let head_tree = repo.find_commit(branch.head)?.tree()?;
|
||||
let diff_stats = repo
|
||||
.diff_tree_to_tree(Some(&base_tree), Some(&head_tree), None)?
|
||||
.stats()?;
|
||||
let branch_data = BranchListingDetails {
|
||||
name: branch.name,
|
||||
lines_added: diff_stats.insertions(),
|
||||
lines_removed: diff_stats.deletions(),
|
||||
number_of_files: diff_stats.files_changed(),
|
||||
};
|
||||
enriched_branches.push(branch_data);
|
||||
}
|
||||
}
|
||||
Ok(enriched_branches)
|
||||
}
|
||||
|
||||
/// Represents a fat struct with all the data associated with a branch
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BranchData {
|
||||
/// The branch that this data is associated with
|
||||
pub branch: BranchListing,
|
||||
/// Sometimes the app creates additional new branches when unapplying a virtual branch, usually suffixed with a counter.
|
||||
/// This is either done by the user to avoid overriding when unapplying or by the app when dealing with conflicts.
|
||||
/// TODO: In general we should make the app not need these and instead have only one associated local branch at any given time.
|
||||
pub local_branches: Vec<LocalBranchEntry>,
|
||||
/// A branch may have multiple remote tracking branches associated with it, from different remotes.
|
||||
/// The name of the branch is the same, but the remote could be different as well as the head commit.
|
||||
pub remote_branches: Vec<RemoteBranchEntry>,
|
||||
/// The virtual branch entry associated with the branch
|
||||
pub virtual_branch: Option<VirtualBranch>,
|
||||
pub struct BranchListingDetails {
|
||||
/// The name of the branch (e.g. `main`, `feature/branch`), excluding the remote name
|
||||
pub name: String,
|
||||
/// The number of lines added within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple).
|
||||
/// If this branch has a virutal branch, lines_added does NOT include the uncommitted lines.
|
||||
pub lines_added: usize,
|
||||
/// The number of lines removed within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number of lines removed,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
/// If this branch has a virutal branch, lines_removed does NOT include the uncommitted lines.
|
||||
pub lines_removed: usize,
|
||||
/// The number of files that were modified within the branch
|
||||
/// Since the virtual branch, local branch and the remote one can have different number files modified,
|
||||
/// the value from the virtual branch (if present) takes the highest precedence,
|
||||
/// followed by the local branch and then the remote branches (taking the max if there are multiple)
|
||||
pub number_of_files: usize,
|
||||
}
|
||||
/// Represents a local branch
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
|
@ -1,3 +1,15 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use gitbutler_branch::{self, dedup, Branch, BranchCreateRequest, BranchId, BranchOwnershipClaims};
|
||||
use gitbutler_commit::commit_headers::HasCommitHeaders;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_oplog::SnapshotExt;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::{rebase::cherry_rebase, RepoActionsExt, RepositoryExt};
|
||||
use gitbutler_time::time::now_since_unix_epoch_ms;
|
||||
|
||||
use super::BranchManager;
|
||||
use crate::{
|
||||
conflicts::{self, RepoConflictsExt},
|
||||
@ -6,18 +18,6 @@ use crate::{
|
||||
integration::update_gitbutler_integration,
|
||||
set_ownership, undo_commit, VirtualBranchesExt,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use gitbutler_branch::{
|
||||
dedup, Branch, BranchOwnershipClaims, {self, BranchCreateRequest, BranchId},
|
||||
};
|
||||
use gitbutler_commit::commit_headers::HasCommitHeaders;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_oplog::SnapshotExt;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::{rebase::cherry_rebase, RepoActionsExt, RepositoryExt};
|
||||
use gitbutler_time::time::now_since_unix_epoch_ms;
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl BranchManager<'_> {
|
||||
pub fn create_virtual_branch(
|
||||
@ -25,12 +25,12 @@ impl BranchManager<'_> {
|
||||
create: &BranchCreateRequest,
|
||||
perm: &mut WorktreeWritePermission,
|
||||
) -> Result<Branch> {
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
let commit = self
|
||||
.project_repository
|
||||
.repo()
|
||||
.ctx
|
||||
.repository()
|
||||
.find_commit(default_target.sha)
|
||||
.context("failed to find default target commit")?;
|
||||
|
||||
@ -54,7 +54,7 @@ impl BranchManager<'_> {
|
||||
);
|
||||
|
||||
_ = self
|
||||
.project_repository
|
||||
.ctx
|
||||
.project()
|
||||
.snapshot_branch_creation(name.clone(), perm);
|
||||
|
||||
@ -107,7 +107,7 @@ impl BranchManager<'_> {
|
||||
ownership: BranchOwnershipClaims::default(),
|
||||
order,
|
||||
selected_for_changes,
|
||||
allow_rebasing: self.project_repository.project().ok_with_force_push.into(),
|
||||
allow_rebasing: self.ctx.project().ok_with_force_push.into(),
|
||||
applied: true,
|
||||
in_workspace: true,
|
||||
not_in_workspace_wip_change_id: None,
|
||||
@ -119,7 +119,7 @@ impl BranchManager<'_> {
|
||||
}
|
||||
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
self.project_repository.add_branch_reference(&branch)?;
|
||||
self.ctx.add_branch_reference(&branch)?;
|
||||
|
||||
Ok(branch)
|
||||
}
|
||||
@ -151,11 +151,11 @@ impl BranchManager<'_> {
|
||||
.to_string();
|
||||
|
||||
let _ = self
|
||||
.project_repository
|
||||
.ctx
|
||||
.project()
|
||||
.snapshot_branch_creation(branch_name.clone(), perm);
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
@ -165,7 +165,7 @@ impl BranchManager<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
let repo = self.project_repository.repo();
|
||||
let repo = self.ctx.repository();
|
||||
let head_reference = repo
|
||||
.find_reference(&target.to_string())
|
||||
.map_err(|err| match err {
|
||||
@ -200,11 +200,8 @@ impl BranchManager<'_> {
|
||||
let merge_base_tree = repo.find_commit(merge_base_oid)?.tree()?;
|
||||
|
||||
// do a diff between the head of this branch and the target base
|
||||
let diff = gitbutler_diff::trees(
|
||||
self.project_repository.repo(),
|
||||
&merge_base_tree,
|
||||
&head_commit_tree,
|
||||
)?;
|
||||
let diff =
|
||||
gitbutler_diff::trees(self.ctx.repository(), &merge_base_tree, &head_commit_tree)?;
|
||||
|
||||
// assign ownership to the branch
|
||||
let ownership = diff.iter().fold(
|
||||
@ -235,7 +232,7 @@ impl BranchManager<'_> {
|
||||
branch.ownership = ownership;
|
||||
branch.order = order;
|
||||
branch.selected_for_changes = selected_for_changes;
|
||||
branch.allow_rebasing = self.project_repository.project().ok_with_force_push.into();
|
||||
branch.allow_rebasing = self.ctx.project().ok_with_force_push.into();
|
||||
branch.applied = true;
|
||||
branch.in_workspace = true;
|
||||
|
||||
@ -255,7 +252,7 @@ impl BranchManager<'_> {
|
||||
ownership,
|
||||
order,
|
||||
selected_for_changes,
|
||||
allow_rebasing: self.project_repository.project().ok_with_force_push.into(),
|
||||
allow_rebasing: self.ctx.project().ok_with_force_push.into(),
|
||||
applied: true,
|
||||
in_workspace: true,
|
||||
not_in_workspace_wip_change_id: None,
|
||||
@ -263,7 +260,7 @@ impl BranchManager<'_> {
|
||||
};
|
||||
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
self.project_repository.add_branch_reference(&branch)?;
|
||||
self.ctx.add_branch_reference(&branch)?;
|
||||
|
||||
match self.apply_branch(branch.id, perm) {
|
||||
Ok(_) => Ok(branch.id),
|
||||
@ -287,11 +284,11 @@ impl BranchManager<'_> {
|
||||
branch_id: BranchId,
|
||||
perm: &mut WorktreeWritePermission,
|
||||
) -> Result<String> {
|
||||
self.project_repository.assure_resolved()?;
|
||||
self.project_repository.assure_unconflicted()?;
|
||||
let repo = self.project_repository.repo();
|
||||
self.ctx.assure_resolved()?;
|
||||
self.ctx.assure_unconflicted()?;
|
||||
let repo = self.ctx.repository();
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
let default_target = vb_state.get_default_target()?;
|
||||
|
||||
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
|
||||
@ -358,11 +355,7 @@ impl BranchManager<'_> {
|
||||
merge_conflicts.push(path);
|
||||
}
|
||||
}
|
||||
conflicts::mark(
|
||||
self.project_repository,
|
||||
&merge_conflicts,
|
||||
Some(default_target.sha),
|
||||
)?;
|
||||
conflicts::mark(self.ctx, &merge_conflicts, Some(default_target.sha))?;
|
||||
|
||||
return Ok(branch.name);
|
||||
}
|
||||
@ -372,7 +365,7 @@ impl BranchManager<'_> {
|
||||
.context("failed to find head commit")?;
|
||||
|
||||
let merged_branch_tree_oid = merge_index
|
||||
.write_tree_to(self.project_repository.repo())
|
||||
.write_tree_to(self.ctx.repository())
|
||||
.context("failed to write tree")?;
|
||||
|
||||
let merged_branch_tree = repo
|
||||
@ -384,7 +377,7 @@ impl BranchManager<'_> {
|
||||
// branch was pushed to upstream, and user doesn't like force pushing.
|
||||
// create a merge commit to avoid the need of force pushing then.
|
||||
|
||||
let new_branch_head = self.project_repository.commit(
|
||||
let new_branch_head = self.ctx.commit(
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
default_target.branch.remote(),
|
||||
@ -401,7 +394,7 @@ impl BranchManager<'_> {
|
||||
branch.head = new_branch_head;
|
||||
} else {
|
||||
let rebase = cherry_rebase(
|
||||
self.project_repository,
|
||||
self.ctx,
|
||||
target_commit.id(),
|
||||
target_commit.id(),
|
||||
branch.head,
|
||||
@ -432,7 +425,7 @@ impl BranchManager<'_> {
|
||||
|
||||
// commit the merge tree oid
|
||||
let new_branch_head = self
|
||||
.project_repository
|
||||
.ctx
|
||||
.commit(
|
||||
format!(
|
||||
"Merged {}/{} into {}",
|
||||
@ -459,7 +452,7 @@ impl BranchManager<'_> {
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
}
|
||||
|
||||
let wd_tree = self.project_repository.repo().get_wd_tree()?;
|
||||
let wd_tree = self.ctx.repository().get_wd_tree()?;
|
||||
|
||||
let branch_tree = repo
|
||||
.find_tree(branch.tree)
|
||||
@ -482,11 +475,7 @@ impl BranchManager<'_> {
|
||||
merge_conflicts.push(path);
|
||||
}
|
||||
}
|
||||
conflicts::mark(
|
||||
self.project_repository,
|
||||
&merge_conflicts,
|
||||
Some(default_target.sha),
|
||||
)?;
|
||||
conflicts::mark(self.ctx, &merge_conflicts, Some(default_target.sha))?;
|
||||
}
|
||||
|
||||
// apply the branch
|
||||
@ -508,7 +497,7 @@ impl BranchManager<'_> {
|
||||
|
||||
if let Some(headers) = potential_wip_commit.gitbutler_headers() {
|
||||
if headers.change_id == wip_commit_to_unapply {
|
||||
undo_commit(self.project_repository, branch.id, branch.head)?;
|
||||
undo_commit(self.ctx, branch.id, branch.head)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -517,7 +506,7 @@ impl BranchManager<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
update_gitbutler_integration(&vb_state, self.project_repository)?;
|
||||
update_gitbutler_integration(&vb_state, self.ctx)?;
|
||||
|
||||
Ok(branch.name)
|
||||
}
|
||||
|
@ -1,5 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_branch::{Branch, BranchExt, BranchId};
|
||||
use gitbutler_commit::commit_headers::CommitHeadersV2;
|
||||
use gitbutler_oplog::SnapshotExt;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_reference::{normalize_branch_name, ReferenceName, Refname};
|
||||
use gitbutler_repo::{RepoActionsExt, RepositoryExt};
|
||||
|
||||
use super::BranchManager;
|
||||
use crate::{
|
||||
conflicts::{self},
|
||||
ensure_selected_for_changes, get_applied_status,
|
||||
@ -7,16 +16,6 @@ use crate::{
|
||||
integration::get_integration_commiter,
|
||||
VirtualBranchesExt,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_branch::{Branch, BranchExt, BranchId};
|
||||
use gitbutler_commit::commit_headers::CommitHeadersV2;
|
||||
use gitbutler_oplog::SnapshotExt;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_reference::ReferenceName;
|
||||
use gitbutler_reference::{normalize_branch_name, Refname};
|
||||
use gitbutler_repo::{RepoActionsExt, RepositoryExt};
|
||||
|
||||
use super::BranchManager;
|
||||
|
||||
impl BranchManager<'_> {
|
||||
// to unapply a branch, we need to write the current tree out, then remove those file changes from the wd
|
||||
@ -25,7 +24,7 @@ impl BranchManager<'_> {
|
||||
branch_id: BranchId,
|
||||
perm: &mut WorktreeWritePermission,
|
||||
) -> Result<ReferenceName> {
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
|
||||
let mut target_branch = vb_state.get_branch(branch_id)?;
|
||||
|
||||
@ -35,8 +34,8 @@ impl BranchManager<'_> {
|
||||
self.delete_branch(branch_id, perm)?;
|
||||
|
||||
// If we were conflicting, it means that it was the only branch applied. Since we've now unapplied it we can clear all conflicts
|
||||
if conflicts::is_conflicting(self.project_repository, None)? {
|
||||
conflicts::clear(self.project_repository)?;
|
||||
if conflicts::is_conflicting(self.ctx, None)? {
|
||||
conflicts::clear(self.ctx)?;
|
||||
}
|
||||
|
||||
vb_state.update_ordering()?;
|
||||
@ -44,7 +43,7 @@ impl BranchManager<'_> {
|
||||
// Ensure we still have a default target
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
|
||||
crate::integration::update_gitbutler_integration(&vb_state, self.project_repository)?;
|
||||
crate::integration::update_gitbutler_integration(&vb_state, self.ctx)?;
|
||||
|
||||
real_branch.reference_name()
|
||||
}
|
||||
@ -54,7 +53,7 @@ impl BranchManager<'_> {
|
||||
branch_id: BranchId,
|
||||
perm: &mut WorktreeWritePermission,
|
||||
) -> Result<()> {
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
let Some(branch) = vb_state.try_branch(branch_id)? else {
|
||||
return Ok(());
|
||||
};
|
||||
@ -65,16 +64,16 @@ impl BranchManager<'_> {
|
||||
}
|
||||
|
||||
_ = self
|
||||
.project_repository
|
||||
.ctx
|
||||
.project()
|
||||
.snapshot_branch_deletion(branch.name.clone(), perm);
|
||||
|
||||
let repo = self.project_repository.repo();
|
||||
let repo = self.ctx.repository();
|
||||
|
||||
let target_commit = repo.target_commit()?;
|
||||
let base_tree = target_commit.tree().context("failed to get target tree")?;
|
||||
|
||||
let applied_statuses = get_applied_status(self.project_repository, None)
|
||||
let applied_statuses = get_applied_status(self.ctx, None)
|
||||
.context("failed to get status by branch")?
|
||||
.branches;
|
||||
|
||||
@ -98,11 +97,8 @@ impl BranchManager<'_> {
|
||||
.into_iter()
|
||||
.map(|file| (file.path, file.hunks))
|
||||
.collect::<Vec<(PathBuf, Vec<VirtualBranchHunk>)>>();
|
||||
let tree_oid = gitbutler_diff::write::hunks_onto_oid(
|
||||
self.project_repository,
|
||||
&branch.head,
|
||||
files,
|
||||
)?;
|
||||
let tree_oid =
|
||||
gitbutler_diff::write::hunks_onto_oid(self.ctx, &branch.head, files)?;
|
||||
let branch_tree = repo.find_tree(tree_oid)?;
|
||||
let mut result =
|
||||
repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?;
|
||||
@ -119,7 +115,7 @@ impl BranchManager<'_> {
|
||||
.checkout()
|
||||
.context("failed to checkout tree")?;
|
||||
|
||||
self.project_repository.delete_branch_reference(&branch)?;
|
||||
self.ctx.delete_branch_reference(&branch)?;
|
||||
|
||||
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
|
||||
|
||||
@ -129,12 +125,12 @@ impl BranchManager<'_> {
|
||||
|
||||
impl BranchManager<'_> {
|
||||
fn build_real_branch(&self, vbranch: &mut Branch) -> Result<git2::Branch<'_>> {
|
||||
let repo = self.project_repository.repo();
|
||||
let repo = self.ctx.repository();
|
||||
let target_commit = repo.find_commit(vbranch.head)?;
|
||||
let branch_name = vbranch.name.clone();
|
||||
let branch_name = normalize_branch_name(&branch_name);
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
let branch = repo.branch(&branch_name, &target_commit, true)?;
|
||||
vbranch.source_refname = Some(Refname::try_from(&branch)?);
|
||||
vb_state.set_branch(vbranch.clone())?;
|
||||
@ -149,7 +145,7 @@ impl BranchManager<'_> {
|
||||
vbranch: &mut Branch,
|
||||
branch: &git2::Branch<'_>,
|
||||
) -> Result<Option<git2::Oid>> {
|
||||
let repo = self.project_repository.repo();
|
||||
let repo = self.ctx.repository();
|
||||
|
||||
// Build wip tree as either any uncommitted changes or an empty tree
|
||||
let vbranch_wip_tree = repo.find_tree(vbranch.tree)?;
|
||||
@ -182,7 +178,7 @@ impl BranchManager<'_> {
|
||||
Some(commit_headers.clone()),
|
||||
)?;
|
||||
|
||||
let vb_state = self.project_repository.project().virtual_branches();
|
||||
let vb_state = self.ctx.project().virtual_branches();
|
||||
// vbranch.head = commit_oid;
|
||||
vbranch.not_in_workspace_wip_change_id = Some(commit_headers.change_id);
|
||||
vb_state.set_branch(vbranch.clone())?;
|
||||
|
@ -1,20 +1,18 @@
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
|
||||
mod branch_creation;
|
||||
mod branch_removal;
|
||||
|
||||
pub struct BranchManager<'l> {
|
||||
project_repository: &'l ProjectRepository,
|
||||
ctx: &'l CommandContext,
|
||||
}
|
||||
|
||||
pub trait BranchManagerExt {
|
||||
fn branch_manager(&self) -> BranchManager;
|
||||
}
|
||||
|
||||
impl BranchManagerExt for ProjectRepository {
|
||||
impl BranchManagerExt for CommandContext {
|
||||
fn branch_manager(&self) -> BranchManager {
|
||||
BranchManager {
|
||||
project_repository: self,
|
||||
}
|
||||
BranchManager { ctx: self }
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
use anyhow::{Context, Result};
|
||||
use bstr::BString;
|
||||
use gitbutler_branch::{Branch, BranchId};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_commit::commit_ext::CommitExt;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
author::Author,
|
||||
file::{list_virtual_commit_files, VirtualBranchFile},
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use bstr::BString;
|
||||
use gitbutler_branch::{Branch, BranchId};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_commit::commit_ext::CommitExt;
|
||||
use serde::Serialize;
|
||||
|
||||
// this is the struct that maps to the view `Commit` type in Typescript
|
||||
// it is derived from walking the git commits between the `Branch.head` commit
|
||||
@ -37,7 +38,7 @@ pub struct VirtualBranchCommit {
|
||||
}
|
||||
|
||||
pub(crate) fn commit_to_vbranch_commit(
|
||||
repository: &ProjectRepository,
|
||||
repository: &CommandContext,
|
||||
branch: &Branch,
|
||||
commit: &git2::Commit,
|
||||
is_integrated: bool,
|
||||
|
@ -1,7 +1,3 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_error::error::Marker;
|
||||
use std::ffi::OsStr;
|
||||
/// stuff to manage merge conflict state.
|
||||
/// This is the dumbest possible way to do this, but it is a placeholder.
|
||||
@ -14,8 +10,13 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_error::error::Marker;
|
||||
|
||||
pub(crate) fn mark<P: AsRef<Path>, A: AsRef<[P]>>(
|
||||
ctx: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
paths: A,
|
||||
parent: Option<git2::Oid>,
|
||||
) -> Result<()> {
|
||||
@ -44,15 +45,15 @@ pub(crate) fn mark<P: AsRef<Path>, A: AsRef<[P]>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn conflicts_path(ctx: &ProjectRepository) -> PathBuf {
|
||||
ctx.repo().path().join("conflicts")
|
||||
fn conflicts_path(ctx: &CommandContext) -> PathBuf {
|
||||
ctx.repository().path().join("conflicts")
|
||||
}
|
||||
|
||||
fn merge_parent_path(ctx: &ProjectRepository) -> PathBuf {
|
||||
ctx.repo().path().join("base_merge_parent")
|
||||
fn merge_parent_path(ctx: &CommandContext) -> PathBuf {
|
||||
ctx.repository().path().join("base_merge_parent")
|
||||
}
|
||||
|
||||
pub(crate) fn merge_parent(ctx: &ProjectRepository) -> Result<Option<git2::Oid>> {
|
||||
pub(crate) fn merge_parent(ctx: &CommandContext) -> Result<Option<git2::Oid>> {
|
||||
use std::io::BufRead;
|
||||
|
||||
let merge_path = merge_parent_path(ctx);
|
||||
@ -72,7 +73,7 @@ pub(crate) fn merge_parent(ctx: &ProjectRepository) -> Result<Option<git2::Oid>>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve<P: AsRef<Path>>(ctx: &ProjectRepository, path_to_resolve: P) -> Result<()> {
|
||||
pub fn resolve<P: AsRef<Path>>(ctx: &CommandContext, path_to_resolve: P) -> Result<()> {
|
||||
let path_to_resolve = path_to_resolve.as_ref();
|
||||
let path_to_resolve = path_to_resolve.as_os_str().as_encoded_bytes();
|
||||
let conflicts_path = conflicts_path(ctx);
|
||||
@ -92,7 +93,7 @@ pub fn resolve<P: AsRef<Path>>(ctx: &ProjectRepository, path_to_resolve: P) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn conflicting_files(ctx: &ProjectRepository) -> Result<Vec<PathBuf>> {
|
||||
pub(crate) fn conflicting_files(ctx: &CommandContext) -> Result<Vec<PathBuf>> {
|
||||
let conflicts_path = conflicts_path(ctx);
|
||||
if !conflicts_path.exists() {
|
||||
return Ok(vec![]);
|
||||
@ -107,7 +108,7 @@ pub(crate) fn conflicting_files(ctx: &ProjectRepository) -> Result<Vec<PathBuf>>
|
||||
|
||||
/// 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(crate) fn is_conflicting(repository: &ProjectRepository, path: Option<&Path>) -> Result<bool> {
|
||||
pub(crate) fn is_conflicting(repository: &CommandContext, path: Option<&Path>) -> Result<bool> {
|
||||
let conflicts_path = conflicts_path(repository);
|
||||
if !conflicts_path.exists() {
|
||||
return Ok(false);
|
||||
@ -127,11 +128,11 @@ pub(crate) fn is_conflicting(repository: &ProjectRepository, path: Option<&Path>
|
||||
|
||||
// is this project still in a resolving conflict state?
|
||||
// - could be that there are no more conflicts, but the state is not committed
|
||||
pub(crate) fn is_resolving(ctx: &ProjectRepository) -> bool {
|
||||
pub(crate) fn is_resolving(ctx: &CommandContext) -> bool {
|
||||
merge_parent_path(ctx).exists()
|
||||
}
|
||||
|
||||
pub(crate) fn clear(ctx: &ProjectRepository) -> Result<()> {
|
||||
pub(crate) fn clear(ctx: &CommandContext) -> Result<()> {
|
||||
remove_file_ignore_missing(merge_parent_path(ctx))?;
|
||||
remove_file_ignore_missing(conflicts_path(ctx))?;
|
||||
Ok(())
|
||||
@ -153,7 +154,7 @@ pub(crate) trait RepoConflictsExt {
|
||||
fn is_resolving(&self) -> bool;
|
||||
}
|
||||
|
||||
impl RepoConflictsExt for ProjectRepository {
|
||||
impl RepoConflictsExt for CommandContext {
|
||||
fn is_resolving(&self) -> bool {
|
||||
is_resolving(self)
|
||||
}
|
||||
|
@ -3,13 +3,15 @@ use std::{
|
||||
path::{self, Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::hunk::{file_hunks_from_diffs, VirtualBranchHunk};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_diff::FileDiff;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::conflicts;
|
||||
use crate::{
|
||||
conflicts,
|
||||
hunk::{file_hunks_from_diffs, VirtualBranchHunk},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -84,7 +86,7 @@ impl Get<VirtualBranchFile> for Vec<VirtualBranchFile> {
|
||||
}
|
||||
|
||||
pub(crate) fn list_virtual_commit_files(
|
||||
project_repository: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
commit: &git2::Commit,
|
||||
) -> Result<Vec<VirtualBranchFile>> {
|
||||
if commit.parent_count() == 0 {
|
||||
@ -93,12 +95,9 @@ pub(crate) fn list_virtual_commit_files(
|
||||
let parent = commit.parent(0).context("failed to get parent commit")?;
|
||||
let commit_tree = commit.tree().context("failed to get commit tree")?;
|
||||
let parent_tree = parent.tree().context("failed to get parent tree")?;
|
||||
let diff = gitbutler_diff::trees(project_repository.repo(), &parent_tree, &commit_tree)?;
|
||||
let hunks_by_filepath = virtual_hunks_by_file_diffs(&project_repository.project().path, diff);
|
||||
Ok(virtual_hunks_into_virtual_files(
|
||||
project_repository,
|
||||
hunks_by_filepath,
|
||||
))
|
||||
let diff = gitbutler_diff::trees(ctx.repository(), &parent_tree, &commit_tree)?;
|
||||
let hunks_by_filepath = virtual_hunks_by_file_diffs(&ctx.project().path, diff);
|
||||
Ok(virtual_hunks_into_virtual_files(ctx, hunks_by_filepath))
|
||||
}
|
||||
|
||||
fn virtual_hunks_by_file_diffs<'a>(
|
||||
@ -115,15 +114,14 @@ fn virtual_hunks_by_file_diffs<'a>(
|
||||
|
||||
/// NOTE: There is no use returning an iterator here as this acts like the final product.
|
||||
pub(crate) fn virtual_hunks_into_virtual_files(
|
||||
project_repository: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
hunks: impl IntoIterator<Item = (PathBuf, Vec<VirtualBranchHunk>)>,
|
||||
) -> Vec<VirtualBranchFile> {
|
||||
hunks
|
||||
.into_iter()
|
||||
.map(|(path, hunks)| {
|
||||
let id = path.display().to_string();
|
||||
let conflicted =
|
||||
conflicts::is_conflicting(project_repository, Some(&path)).unwrap_or(false);
|
||||
let conflicted = conflicts::is_conflicting(ctx, Some(&path)).unwrap_or(false);
|
||||
let binary = hunks.iter().any(|h| h.binary);
|
||||
let modified_at = hunks.iter().map(|h| h.modified_at).max().unwrap_or(0);
|
||||
debug_assert!(hunks.iter().all(|hunk| hunk.file_path == path));
|
||||
|
@ -2,6 +2,7 @@ use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
time,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use bstr::BString;
|
||||
@ -10,7 +11,6 @@ use gitbutler_diff::{GitHunk, Hunk, HunkHash};
|
||||
use itertools::Itertools;
|
||||
use md5::Digest;
|
||||
use serde::Serialize;
|
||||
use std::time::SystemTime;
|
||||
|
||||
// this struct is a mapping to the view `Hunk` type in Typescript
|
||||
// found in src-tauri/src/routes/repo/[project_id]/types.ts
|
||||
|
@ -2,21 +2,18 @@ use std::{path::PathBuf, vec};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
|
||||
use gitbutler_branch::{self, BranchCreateRequest};
|
||||
use gitbutler_branch::{Branch, VirtualBranchesHandle};
|
||||
use gitbutler_branch::{
|
||||
self, Branch, BranchCreateRequest, VirtualBranchesHandle,
|
||||
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
|
||||
GITBUTLER_INTEGRATION_REFERENCE,
|
||||
};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_commit::commit_ext::CommitExt;
|
||||
use gitbutler_error::error::Marker;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
|
||||
|
||||
use crate::branch_manager::BranchManagerExt;
|
||||
use crate::{conflicts, VirtualBranchesExt};
|
||||
use crate::{branch_manager::BranchManagerExt, conflicts, VirtualBranchesExt};
|
||||
|
||||
const WORKSPACE_HEAD: &str = "Workspace Head";
|
||||
|
||||
@ -31,12 +28,12 @@ pub(crate) fn get_integration_commiter<'a>() -> Result<git2::Signature<'a>> {
|
||||
//
|
||||
// This is the base against which we diff the working directory to understand
|
||||
// what files have been modified.
|
||||
pub(crate) fn get_workspace_head(project_repo: &ProjectRepository) -> Result<git2::Oid> {
|
||||
let vb_state = project_repo.project().virtual_branches();
|
||||
pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result<git2::Oid> {
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
let target = vb_state
|
||||
.get_default_target()
|
||||
.context("failed to get target")?;
|
||||
let repo: &git2::Repository = project_repo.repo();
|
||||
let repo: &git2::Repository = ctx.repository();
|
||||
|
||||
let mut virtual_branches: Vec<Branch> = vb_state.list_branches_in_workspace()?;
|
||||
|
||||
@ -54,9 +51,8 @@ pub(crate) fn get_workspace_head(project_repo: &ProjectRepository) -> Result<git
|
||||
return Ok(target_commit.id());
|
||||
}
|
||||
|
||||
if conflicts::is_conflicting(project_repo, None)? {
|
||||
let merge_parent =
|
||||
conflicts::merge_parent(project_repo)?.ok_or(anyhow!("No merge parent"))?;
|
||||
if conflicts::is_conflicting(ctx, None)? {
|
||||
let merge_parent = conflicts::merge_parent(ctx)?.ok_or(anyhow!("No merge parent"))?;
|
||||
let first_branch = virtual_branches.first().ok_or(anyhow!("No branches"))?;
|
||||
|
||||
let merge_base = repo.merge_base(first_branch.head, merge_parent)?;
|
||||
@ -138,13 +134,13 @@ fn write_integration_file(head: &git2::Reference, path: PathBuf) -> Result<()> {
|
||||
}
|
||||
pub fn update_gitbutler_integration(
|
||||
vb_state: &VirtualBranchesHandle,
|
||||
project_repository: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
) -> Result<git2::Oid> {
|
||||
let target = vb_state
|
||||
.get_default_target()
|
||||
.context("failed to get target")?;
|
||||
|
||||
let repo: &git2::Repository = project_repository.repo();
|
||||
let repo: &git2::Repository = ctx.repository();
|
||||
|
||||
// get commit object from target.sha
|
||||
let target_commit = repo.find_commit(target.sha)?;
|
||||
@ -165,14 +161,14 @@ pub fn update_gitbutler_integration(
|
||||
}
|
||||
}
|
||||
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
|
||||
// get all virtual branches, we need to try to update them all
|
||||
let virtual_branches: Vec<Branch> = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("failed to list virtual branches")?;
|
||||
|
||||
let integration_commit = repo.find_commit(get_workspace_head(project_repository)?)?;
|
||||
let integration_commit = repo.find_commit(get_workspace_head(ctx)?)?;
|
||||
let integration_tree = integration_commit.tree()?;
|
||||
|
||||
// message that says how to get back to where they were
|
||||
@ -282,7 +278,7 @@ pub fn update_gitbutler_integration(
|
||||
Ok(final_commit)
|
||||
}
|
||||
|
||||
pub fn verify_branch(ctx: &ProjectRepository, perm: &mut WorktreeWritePermission) -> Result<()> {
|
||||
pub fn verify_branch(ctx: &CommandContext, perm: &mut WorktreeWritePermission) -> Result<()> {
|
||||
verify_current_branch_name(ctx)
|
||||
.and_then(verify_head_is_set)
|
||||
.and_then(|()| verify_head_is_clean(ctx, perm))
|
||||
@ -290,8 +286,13 @@ pub fn verify_branch(ctx: &ProjectRepository, perm: &mut WorktreeWritePermission
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_head_is_set(ctx: &ProjectRepository) -> Result<()> {
|
||||
match ctx.repo().head().context("failed to get head")?.name() {
|
||||
fn verify_head_is_set(ctx: &CommandContext) -> Result<()> {
|
||||
match ctx
|
||||
.repository()
|
||||
.head()
|
||||
.context("failed to get head")?
|
||||
.name()
|
||||
{
|
||||
Some(refname) if *refname == GITBUTLER_INTEGRATION_REFERENCE.to_string() => Ok(()),
|
||||
Some(head_name) => Err(invalid_head_err(head_name)),
|
||||
None => Err(anyhow!(
|
||||
@ -302,8 +303,8 @@ fn verify_head_is_set(ctx: &ProjectRepository) -> Result<()> {
|
||||
}
|
||||
|
||||
// Returns an error if repo head is not pointing to the integration branch.
|
||||
fn verify_current_branch_name(ctx: &ProjectRepository) -> Result<&ProjectRepository> {
|
||||
match ctx.repo().head()?.name() {
|
||||
fn verify_current_branch_name(ctx: &CommandContext) -> Result<&CommandContext> {
|
||||
match ctx.repository().head()?.name() {
|
||||
Some(head) => {
|
||||
let head_name = head.to_string();
|
||||
if head_name != GITBUTLER_INTEGRATION_REFERENCE.to_string() {
|
||||
@ -316,9 +317,9 @@ fn verify_current_branch_name(ctx: &ProjectRepository) -> Result<&ProjectReposit
|
||||
}
|
||||
|
||||
// TODO(ST): Probably there should not be an implicit vbranch creation here.
|
||||
fn verify_head_is_clean(ctx: &ProjectRepository, perm: &mut WorktreeWritePermission) -> Result<()> {
|
||||
fn verify_head_is_clean(ctx: &CommandContext, perm: &mut WorktreeWritePermission) -> Result<()> {
|
||||
let head_commit = ctx
|
||||
.repo()
|
||||
.repository()
|
||||
.head()
|
||||
.context("failed to get head")?
|
||||
.peel_to_commit()
|
||||
@ -345,7 +346,7 @@ fn verify_head_is_clean(ctx: &ProjectRepository, perm: &mut WorktreeWritePermiss
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ctx.repo()
|
||||
ctx.repository()
|
||||
.reset(
|
||||
integration_commit.as_ref().unwrap().as_object(),
|
||||
git2::ResetType::Soft,
|
||||
@ -372,12 +373,12 @@ fn verify_head_is_clean(ctx: &ProjectRepository, perm: &mut WorktreeWritePermiss
|
||||
let mut head = new_branch.head;
|
||||
for commit in extra_commits {
|
||||
let new_branch_head = ctx
|
||||
.repo()
|
||||
.repository()
|
||||
.find_commit(head)
|
||||
.context("failed to find new branch head")?;
|
||||
|
||||
let rebased_commit_oid = ctx
|
||||
.repo()
|
||||
.repository()
|
||||
.commit_with_signature(
|
||||
None,
|
||||
&commit.author(),
|
||||
@ -392,10 +393,13 @@ fn verify_head_is_clean(ctx: &ProjectRepository, perm: &mut WorktreeWritePermiss
|
||||
commit.id()
|
||||
))?;
|
||||
|
||||
let rebased_commit = ctx.repo().find_commit(rebased_commit_oid).context(format!(
|
||||
"failed to find rebased commit {}",
|
||||
rebased_commit_oid
|
||||
))?;
|
||||
let rebased_commit = ctx
|
||||
.repository()
|
||||
.find_commit(rebased_commit_oid)
|
||||
.context(format!(
|
||||
"failed to find rebased commit {}",
|
||||
rebased_commit_oid
|
||||
))?;
|
||||
|
||||
new_branch.head = rebased_commit.id();
|
||||
new_branch.tree = rebased_commit.tree_id();
|
||||
|
@ -15,8 +15,7 @@ mod integration;
|
||||
pub use integration::{update_gitbutler_integration, verify_branch};
|
||||
|
||||
mod file;
|
||||
pub use file::Get;
|
||||
pub use file::RemoteBranchFile;
|
||||
pub use file::{Get, RemoteBranchFile};
|
||||
|
||||
mod remote;
|
||||
pub use remote::{list_remote_branches, RemoteBranch, RemoteBranchData, RemoteCommit};
|
||||
@ -25,9 +24,8 @@ pub mod conflicts;
|
||||
|
||||
mod author;
|
||||
mod status;
|
||||
pub use status::get_applied_status;
|
||||
|
||||
use gitbutler_branch::VirtualBranchesHandle;
|
||||
pub use status::get_applied_status;
|
||||
trait VirtualBranchesExt {
|
||||
fn virtual_branches(&self) -> VirtualBranchesHandle;
|
||||
}
|
||||
@ -42,4 +40,7 @@ mod branch;
|
||||
mod commit;
|
||||
mod hunk;
|
||||
|
||||
pub use branch::{list_branches, BranchListing};
|
||||
pub use branch::{
|
||||
get_branch_listing_details, list_branches, Author, BranchListing, BranchListingDetails,
|
||||
BranchListingFilter,
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ use std::path::Path;
|
||||
use anyhow::{Context, Result};
|
||||
use bstr::BString;
|
||||
use gitbutler_branch::{ReferenceExt, Target, VirtualBranchesHandle};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_commit::commit_ext::CommitExt;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt};
|
||||
@ -61,17 +61,17 @@ pub struct RemoteCommit {
|
||||
|
||||
// for legacy purposes, this is still named "remote" branches, but it's actually
|
||||
// a list of all the normal (non-gitbutler) git branches.
|
||||
pub fn list_remote_branches(project_repository: &ProjectRepository) -> Result<Vec<RemoteBranch>> {
|
||||
let default_target = default_target(&project_repository.project().gb_dir())?;
|
||||
pub fn list_remote_branches(ctx: &CommandContext) -> Result<Vec<RemoteBranch>> {
|
||||
let default_target = default_target(&ctx.project().gb_dir())?;
|
||||
|
||||
let mut remote_branches = vec![];
|
||||
for (branch, _) in project_repository
|
||||
.repo()
|
||||
for (branch, _) in ctx
|
||||
.repository()
|
||||
.branches(None)
|
||||
.context("failed to list remote branches")?
|
||||
.flatten()
|
||||
{
|
||||
let branch = branch_to_remote_branch(project_repository, &branch);
|
||||
let branch = branch_to_remote_branch(ctx, &branch);
|
||||
|
||||
if let Some(branch) = branch {
|
||||
let branch_is_trunk = branch.name.branch() == Some(default_target.branch.branch())
|
||||
@ -88,14 +88,11 @@ pub fn list_remote_branches(project_repository: &ProjectRepository) -> Result<Ve
|
||||
Ok(remote_branches)
|
||||
}
|
||||
|
||||
pub(crate) fn get_branch_data(
|
||||
ctx: &ProjectRepository,
|
||||
refname: &Refname,
|
||||
) -> Result<RemoteBranchData> {
|
||||
pub(crate) fn get_branch_data(ctx: &CommandContext, refname: &Refname) -> Result<RemoteBranchData> {
|
||||
let default_target = default_target(&ctx.project().gb_dir())?;
|
||||
|
||||
let branch = ctx
|
||||
.repo()
|
||||
.repository()
|
||||
.find_branch_by_refname(refname)?
|
||||
.ok_or(anyhow::anyhow!("failed to find branch {}", refname))?;
|
||||
|
||||
@ -104,7 +101,7 @@ pub(crate) fn get_branch_data(
|
||||
}
|
||||
|
||||
pub(crate) fn branch_to_remote_branch(
|
||||
ctx: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
branch: &git2::Branch,
|
||||
) -> Option<RemoteBranch> {
|
||||
let commit = match branch.get().peel_to_commit() {
|
||||
@ -122,7 +119,10 @@ pub(crate) fn branch_to_remote_branch(
|
||||
.context("could not get branch name")
|
||||
.ok()?;
|
||||
|
||||
let given_name = branch.get().given_name(&ctx.repo().remotes().ok()?).ok()?;
|
||||
let given_name = branch
|
||||
.get()
|
||||
.given_name(&ctx.repository().remotes().ok()?)
|
||||
.ok()?;
|
||||
|
||||
branch.get().target().map(|sha| RemoteBranch {
|
||||
sha,
|
||||
@ -145,7 +145,7 @@ pub(crate) fn branch_to_remote_branch(
|
||||
}
|
||||
|
||||
pub(crate) fn branch_to_remote_branch_data(
|
||||
project_repository: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
branch: &git2::Branch,
|
||||
base: git2::Oid,
|
||||
) -> Result<Option<RemoteBranchData>> {
|
||||
@ -153,13 +153,13 @@ pub(crate) fn branch_to_remote_branch_data(
|
||||
.get()
|
||||
.target()
|
||||
.map(|sha| {
|
||||
let ahead = project_repository
|
||||
let ahead = ctx
|
||||
.log(sha, LogUntil::Commit(base))
|
||||
.context("failed to get ahead commits")?;
|
||||
|
||||
let name = Refname::try_from(branch).context("could not get branch name")?;
|
||||
|
||||
let count_behind = project_repository
|
||||
let count_behind = ctx
|
||||
.distance(base, sha)
|
||||
.context("failed to get behind count")?;
|
||||
|
||||
|
@ -1,18 +1,19 @@
|
||||
use std::{collections::HashMap, path::PathBuf, vec};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use gitbutler_branch::{
|
||||
Branch, BranchCreateRequest, BranchId, BranchOwnershipClaims, OwnershipClaim,
|
||||
};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_diff::{diff_files_into_hunks, GitHunk, Hunk, HunkHash};
|
||||
use gitbutler_operating_modes::assure_open_workspace_mode;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_repo::RepositoryExt;
|
||||
use std::{collections::HashMap, path::PathBuf, vec};
|
||||
|
||||
use crate::file::{virtual_hunks_into_virtual_files, VirtualBranchFile};
|
||||
use crate::hunk::file_hunks_from_diffs;
|
||||
use crate::{
|
||||
conflicts::RepoConflictsExt,
|
||||
hunk::{HunkLock, VirtualBranchHunk},
|
||||
file::{virtual_hunks_into_virtual_files, VirtualBranchFile},
|
||||
hunk::{file_hunks_from_diffs, HunkLock, VirtualBranchHunk},
|
||||
integration::get_workspace_head,
|
||||
BranchManagerExt, VirtualBranchesExt,
|
||||
};
|
||||
@ -29,17 +30,18 @@ pub struct VirtualBranchesStatus {
|
||||
/// of skipped files.
|
||||
// TODO(kv): make this side effect free
|
||||
pub fn get_applied_status(
|
||||
project_repository: &ProjectRepository,
|
||||
ctx: &CommandContext,
|
||||
perm: Option<&mut WorktreeWritePermission>,
|
||||
) -> Result<VirtualBranchesStatus> {
|
||||
let integration_commit = get_workspace_head(project_repository)?;
|
||||
let mut virtual_branches = project_repository
|
||||
assure_open_workspace_mode(ctx)
|
||||
.context("Getting applied status requires open workspace mode")?;
|
||||
let integration_commit = get_workspace_head(ctx)?;
|
||||
let mut virtual_branches = ctx
|
||||
.project()
|
||||
.virtual_branches()
|
||||
.list_branches_in_workspace()?;
|
||||
let base_file_diffs =
|
||||
gitbutler_diff::workdir(project_repository.repo(), &integration_commit.to_owned())
|
||||
.context("failed to diff workdir")?;
|
||||
let base_file_diffs = gitbutler_diff::workdir(ctx.repository(), &integration_commit.to_owned())
|
||||
.context("failed to diff workdir")?;
|
||||
|
||||
let mut skipped_files: Vec<gitbutler_diff::FileDiff> = Vec::new();
|
||||
for file_diff in base_file_diffs.values() {
|
||||
@ -52,7 +54,7 @@ pub fn get_applied_status(
|
||||
// sort by order, so that the default branch is first (left in the ui)
|
||||
virtual_branches.sort_by(|a, b| a.order.cmp(&b.order));
|
||||
|
||||
let branch_manager = project_repository.branch_manager();
|
||||
let branch_manager = ctx.branch_manager();
|
||||
|
||||
if virtual_branches.is_empty() && !base_diffs.is_empty() {
|
||||
if let Some(perm) = perm {
|
||||
@ -70,7 +72,7 @@ pub fn get_applied_status(
|
||||
.map(|branch| (branch.id, HashMap::new()))
|
||||
.collect();
|
||||
|
||||
let locks = compute_locks(project_repository.repo(), &base_diffs, &virtual_branches)?;
|
||||
let locks = compute_locks(ctx.repository(), &base_diffs, &virtual_branches)?;
|
||||
|
||||
for branch in &mut virtual_branches {
|
||||
let old_claims = branch.ownership.claims.clone();
|
||||
@ -187,11 +189,10 @@ pub fn get_applied_status(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// write updated state if not resolving
|
||||
if !project_repository.is_resolving() {
|
||||
let vb_state = project_repository.project().virtual_branches();
|
||||
if !ctx.is_resolving() {
|
||||
let vb_state = ctx.project().virtual_branches();
|
||||
for (vbranch, files) in &mut hunks_by_branch {
|
||||
vbranch.tree =
|
||||
gitbutler_diff::write::hunks_onto_oid(project_repository, &vbranch.head, files)?;
|
||||
vbranch.tree = gitbutler_diff::write::hunks_onto_oid(ctx, &vbranch.head, files)?;
|
||||
vb_state
|
||||
.set_branch(vbranch.clone())
|
||||
.context(format!("failed to write virtual branch {}", vbranch.name))?;
|
||||
@ -200,11 +201,7 @@ pub fn get_applied_status(
|
||||
let hunks_by_branch: Vec<(Branch, HashMap<PathBuf, Vec<VirtualBranchHunk>>)> = hunks_by_branch
|
||||
.iter()
|
||||
.map(|(branch, hunks)| {
|
||||
let hunks = file_hunks_from_diffs(
|
||||
&project_repository.project().path,
|
||||
hunks.clone(),
|
||||
Some(&locks),
|
||||
);
|
||||
let hunks = file_hunks_from_diffs(&ctx.project().path, hunks.clone(), Some(&locks));
|
||||
(branch.clone(), hunks)
|
||||
})
|
||||
.collect();
|
||||
@ -212,7 +209,7 @@ pub fn get_applied_status(
|
||||
let files_by_branch: Vec<(Branch, Vec<VirtualBranchFile>)> = hunks_by_branch
|
||||
.iter()
|
||||
.map(|(branch, hunks)| {
|
||||
let files = virtual_hunks_into_virtual_files(project_repository, hunks.clone());
|
||||
let files = virtual_hunks_into_virtual_files(ctx, hunks.clone());
|
||||
(branch.clone(), files)
|
||||
})
|
||||
.collect();
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
39
crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh
vendored
Normal file
39
crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
CLI=${1:?The first argument is the GitButler CLI}
|
||||
|
||||
git init remote
|
||||
(cd remote
|
||||
echo first > file
|
||||
git add . && git commit -m "init"
|
||||
)
|
||||
|
||||
git clone remote single-branch-no-vbranch
|
||||
|
||||
git clone remote single-branch-no-vbranch-one-commit
|
||||
(cd single-branch-no-vbranch-one-commit
|
||||
echo change >> file && git add . && git commit -m "local change"
|
||||
)
|
||||
|
||||
git clone remote single-branch-no-vbranch-multi-remote
|
||||
(cd single-branch-no-vbranch-multi-remote
|
||||
git remote add other-origin ../remote
|
||||
git fetch other-origin
|
||||
)
|
||||
|
||||
export GITBUTLER_CLI_DATA_DIR=./git/gitbutler/app-data
|
||||
git clone remote one-vbranch-on-integration
|
||||
(cd one-vbranch-on-integration
|
||||
$CLI project add --switch-to-integration "$(git rev-parse --symbolic-full-name @{u})"
|
||||
$CLI branch create virtual
|
||||
)
|
||||
|
||||
git clone remote one-vbranch-on-integration-one-commit
|
||||
(cd one-vbranch-on-integration-one-commit
|
||||
$CLI project add --switch-to-integration "$(git rev-parse --symbolic-full-name @{u})"
|
||||
$CLI branch create virtual
|
||||
echo change >> file
|
||||
echo in-index > new && git add new
|
||||
$CLI branch commit virtual -m "virtual branch change in index and worktree"
|
||||
)
|
||||
|
@ -1,10 +1,9 @@
|
||||
use gitbutler_branch::BranchOwnershipClaims;
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchOwnershipClaims, BranchUpdateRequest};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn forcepush_allowed() {
|
||||
#[test]
|
||||
fn forcepush_allowed() {
|
||||
let Test {
|
||||
repository,
|
||||
project_id,
|
||||
@ -19,12 +18,10 @@ async fn forcepush_allowed() {
|
||||
id: *project_id,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
projects
|
||||
@ -32,24 +29,20 @@ async fn forcepush_allowed() {
|
||||
id: *project_id,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let commit_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.push_virtual_branch(project, branch_id, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
@ -58,12 +51,10 @@ async fn forcepush_allowed() {
|
||||
let to_amend: BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.amend(project, branch_id, commit_id, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -76,8 +67,8 @@ async fn forcepush_allowed() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forcepush_forbidden() {
|
||||
#[test]
|
||||
fn forcepush_forbidden() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -87,12 +78,10 @@ async fn forcepush_forbidden() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
@ -104,19 +93,16 @@ async fn forcepush_forbidden() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let commit_oid = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.push_virtual_branch(project, branch_id, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
@ -125,7 +111,6 @@ async fn forcepush_forbidden() {
|
||||
assert_eq!(
|
||||
controller
|
||||
.amend(project, branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"force-push is not allowed"
|
||||
@ -133,8 +118,8 @@ async fn forcepush_forbidden() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_locked_hunk() {
|
||||
#[test]
|
||||
fn non_locked_hunk() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -144,24 +129,20 @@ async fn non_locked_hunk() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let commit_oid = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -177,12 +158,10 @@ async fn non_locked_hunk() {
|
||||
let to_amend: BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.amend(project, branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -194,8 +173,8 @@ async fn non_locked_hunk() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn locked_hunk() {
|
||||
#[test]
|
||||
fn locked_hunk() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -205,24 +184,20 @@ async fn locked_hunk() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let commit_oid = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -242,12 +217,10 @@ async fn locked_hunk() {
|
||||
let to_amend: BranchOwnershipClaims = "file.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.amend(project, branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -264,8 +237,8 @@ async fn locked_hunk() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_existing_ownership() {
|
||||
#[test]
|
||||
fn non_existing_ownership() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -275,24 +248,20 @@ async fn non_existing_ownership() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let commit_oid = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -308,7 +277,6 @@ async fn non_existing_ownership() {
|
||||
assert_eq!(
|
||||
controller
|
||||
.amend(project, branch_id, commit_oid, &to_amend)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"target ownership not found"
|
||||
|
@ -3,8 +3,8 @@ use gitbutler_reference::Refname;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn rebase_commit() {
|
||||
#[test]
|
||||
fn rebase_commit() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -25,23 +25,20 @@ async fn rebase_commit() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut branch1_id = {
|
||||
// create a branch with some commited work
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs::write(repository.path().join("another_file.txt"), "virtual").unwrap();
|
||||
|
||||
controller
|
||||
.create_commit(project, branch1_id, "virtual commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert!(branches[0].active);
|
||||
@ -55,7 +52,6 @@ async fn rebase_commit() {
|
||||
// unapply first vbranch
|
||||
let unapplied_branch = controller
|
||||
.convert_to_real_branch(project, branch1_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@ -67,7 +63,7 @@ async fn rebase_commit() {
|
||||
"one"
|
||||
);
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
|
||||
Refname::from_str(&unapplied_branch).unwrap()
|
||||
@ -75,10 +71,10 @@ async fn rebase_commit() {
|
||||
|
||||
{
|
||||
// fetch remote
|
||||
controller.update_base_branch(project).await.unwrap();
|
||||
controller.update_base_branch(project).unwrap();
|
||||
|
||||
// branch is stil unapplied
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
|
||||
assert_eq!(
|
||||
@ -95,11 +91,10 @@ async fn rebase_commit() {
|
||||
// apply first vbranch again
|
||||
branch1_id = controller
|
||||
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// it should be rebased
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].files.len(), 0);
|
||||
@ -119,8 +114,8 @@ async fn rebase_commit() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rebase_work() {
|
||||
#[test]
|
||||
fn rebase_work() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -139,18 +134,16 @@ async fn rebase_work() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut branch1_id = {
|
||||
// make a branch with some work
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs::write(repository.path().join("another_file.txt"), "").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert!(branches[0].active);
|
||||
@ -164,10 +157,9 @@ async fn rebase_work() {
|
||||
// unapply first vbranch
|
||||
let unapplied_branch = controller
|
||||
.convert_to_real_branch(project, branch1_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
|
||||
assert!(!repository.path().join("another_file.txt").exists());
|
||||
@ -178,10 +170,10 @@ async fn rebase_work() {
|
||||
|
||||
{
|
||||
// fetch remote
|
||||
controller.update_base_branch(project).await.unwrap();
|
||||
controller.update_base_branch(project).unwrap();
|
||||
|
||||
// first branch is stil unapplied
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
|
||||
assert!(!repository.path().join("another_file.txt").exists());
|
||||
@ -192,11 +184,10 @@ async fn rebase_work() {
|
||||
// apply first vbranch again
|
||||
branch1_id = controller
|
||||
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// workdir should be rebased, and work should be restored
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
// TODO: Should be 1
|
||||
|
@ -3,8 +3,8 @@ use gitbutler_reference::Refname;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn unapply_with_data() {
|
||||
#[test]
|
||||
fn unapply_with_data() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -14,27 +14,25 @@ async fn unapply_with_data() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
controller
|
||||
.convert_to_real_branch(project, branches[0].id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!repository.path().join("file.txt").exists());
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conflicting() {
|
||||
#[test]
|
||||
fn conflicting() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -54,7 +52,6 @@ async fn conflicting() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let unapplied_branch = {
|
||||
@ -62,7 +59,7 @@ async fn conflicting() {
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert!(branches[0].base_current);
|
||||
assert!(branches[0].active);
|
||||
@ -73,7 +70,6 @@ async fn conflicting() {
|
||||
|
||||
let unapplied_branch = controller
|
||||
.convert_to_real_branch(project, branches[0].id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Refname::from_str(&unapplied_branch).unwrap()
|
||||
@ -81,7 +77,7 @@ async fn conflicting() {
|
||||
|
||||
{
|
||||
// update base branch, causing conflict
|
||||
controller.update_base_branch(project).await.unwrap();
|
||||
controller.update_base_branch(project).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(repository.path().join("file.txt")).unwrap(),
|
||||
@ -93,7 +89,6 @@ async fn conflicting() {
|
||||
// apply branch, it should conflict
|
||||
let branch_id = controller
|
||||
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@ -101,7 +96,7 @@ async fn conflicting() {
|
||||
"<<<<<<< ours\nconflict\n=======\nsecond\n>>>>>>> theirs\n"
|
||||
);
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
|
||||
assert_eq!(branches.len(), 1);
|
||||
let branch = &branches[0];
|
||||
@ -119,7 +114,6 @@ async fn conflicting() {
|
||||
// Converting the branch to a real branch should put us back in an unconflicted state
|
||||
controller
|
||||
.convert_to_real_branch(project, branch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@ -129,8 +123,8 @@ async fn conflicting() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_if_empty() {
|
||||
#[test]
|
||||
fn delete_if_empty() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -139,22 +133,19 @@ async fn delete_if_empty() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
controller
|
||||
.convert_to_real_branch(project, branches[0].id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ use gitbutler_id::id::Id;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_lock_updated_hunks() {
|
||||
#[test]
|
||||
fn should_lock_updated_hunks() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -15,19 +15,17 @@ async fn should_lock_updated_hunks() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// by default, hunks are not locked
|
||||
repository.write_file("file.txt", &["content".to_string()]);
|
||||
|
||||
let branch = get_virtual_branch(controller, project, branch_id).await;
|
||||
let branch = get_virtual_branch(controller, project, branch_id);
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
assert_eq!(branch.files[0].path.display().to_string(), "file.txt");
|
||||
assert_eq!(branch.files[0].hunks.len(), 1);
|
||||
@ -36,7 +34,6 @@ async fn should_lock_updated_hunks() {
|
||||
|
||||
controller
|
||||
.create_commit(project, branch_id, "test", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
@ -45,7 +42,6 @@ async fn should_lock_updated_hunks() {
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -58,8 +54,8 @@ async fn should_lock_updated_hunks() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_reset_into_same_branch() {
|
||||
#[test]
|
||||
fn should_reset_into_same_branch() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -72,12 +68,10 @@ async fn should_reset_into_same_branch() {
|
||||
|
||||
let base_branch = controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_2_id = controller
|
||||
@ -88,7 +82,6 @@ async fn should_reset_into_same_branch() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
lines[0] = "change 1".to_string();
|
||||
@ -96,12 +89,9 @@ async fn should_reset_into_same_branch() {
|
||||
|
||||
controller
|
||||
.create_commit(project, branch_2_id, "commit to branch 2", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let files = get_virtual_branch(controller, project, branch_2_id)
|
||||
.await
|
||||
.files;
|
||||
let files = get_virtual_branch(controller, project, branch_2_id).files;
|
||||
assert_eq!(files.len(), 0);
|
||||
|
||||
// Set target to branch 1 and verify the file resets into branch 2.
|
||||
@ -114,17 +104,13 @@ async fn should_reset_into_same_branch() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.reset_virtual_branch(project, branch_2_id, base_branch.base_sha)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let files = get_virtual_branch(controller, project, branch_2_id)
|
||||
.await
|
||||
.files;
|
||||
let files = get_virtual_branch(controller, project, branch_2_id).files;
|
||||
assert_eq!(files.len(), 1);
|
||||
}
|
||||
|
||||
@ -133,14 +119,13 @@ fn commit_and_push_initial(repository: &TestProject) {
|
||||
repository.push();
|
||||
}
|
||||
|
||||
async fn get_virtual_branch(
|
||||
fn get_virtual_branch(
|
||||
controller: &VirtualBranchActions,
|
||||
project: &Project,
|
||||
branch_id: Id<Branch>,
|
||||
) -> VirtualBranch {
|
||||
controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
|
@ -3,8 +3,8 @@ use gitbutler_reference::LocalRefname;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration() {
|
||||
#[test]
|
||||
fn integration() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -14,7 +14,6 @@ async fn integration() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_name = {
|
||||
@ -22,22 +21,18 @@ async fn integration() {
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "first\n").unwrap();
|
||||
controller
|
||||
.create_commit(project, branch_id, "first", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
controller
|
||||
.push_virtual_branch(project, branch_id, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -48,7 +43,6 @@ async fn integration() {
|
||||
|
||||
controller
|
||||
.delete_virtual_branch(project, branch_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
name
|
||||
@ -57,7 +51,6 @@ async fn integration() {
|
||||
// checkout a existing remote branch
|
||||
let branch_id = controller
|
||||
.create_virtual_branch_from_branch(project, &branch_name, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
@ -66,7 +59,6 @@ async fn integration() {
|
||||
|
||||
controller
|
||||
.create_commit(project, branch_id, "second", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@ -83,12 +75,10 @@ async fn integration() {
|
||||
// merge branch into master
|
||||
controller
|
||||
.push_virtual_branch(project, branch_id, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -105,11 +95,10 @@ async fn integration() {
|
||||
|
||||
{
|
||||
// should mark commits as integrated
|
||||
controller.fetch_from_remotes(project, None).await.unwrap();
|
||||
controller.fetch_from_remotes(project, None).unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -123,8 +112,8 @@ async fn integration() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_conflicts() {
|
||||
#[test]
|
||||
fn no_conflicts() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -144,10 +133,9 @@ async fn no_conflicts() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert!(branches.is_empty());
|
||||
|
||||
let branch_id = controller
|
||||
@ -156,18 +144,17 @@ async fn no_conflicts() {
|
||||
&"refs/remotes/origin/branch".parse().unwrap(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
assert_eq!(branches[0].commits[0].description, "first");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conflicts_with_uncommited() {
|
||||
#[test]
|
||||
fn conflicts_with_uncommited() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -187,14 +174,13 @@ async fn conflicts_with_uncommited() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create a local branch that conflicts with remote
|
||||
{
|
||||
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
};
|
||||
|
||||
@ -206,11 +192,9 @@ async fn conflicts_with_uncommited() {
|
||||
&"refs/remotes/origin/branch".parse().unwrap(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let new_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -221,8 +205,8 @@ async fn conflicts_with_uncommited() {
|
||||
assert!(new_branch.upstream.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn conflicts_with_commited() {
|
||||
#[test]
|
||||
fn conflicts_with_commited() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -242,19 +226,17 @@ async fn conflicts_with_commited() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create a local branch that conflicts with remote
|
||||
{
|
||||
std::fs::write(repository.path().join("file.txt"), "conflict").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
controller
|
||||
.create_commit(project, branches[0].id, "hej", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
@ -266,11 +248,9 @@ async fn conflicts_with_commited() {
|
||||
&"refs/remotes/origin/branch".parse().unwrap(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let new_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -281,8 +261,8 @@ async fn conflicts_with_commited() {
|
||||
assert!(new_branch.upstream.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn from_default_target() {
|
||||
#[test]
|
||||
fn from_default_target() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -291,7 +271,6 @@ async fn from_default_target() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// branch should be created unapplied, because of the conflict
|
||||
@ -303,15 +282,14 @@ async fn from_default_target() {
|
||||
&"refs/remotes/origin/master".parse().unwrap(),
|
||||
None
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"cannot create a branch from default target"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn from_non_existent_branch() {
|
||||
#[test]
|
||||
fn from_non_existent_branch() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -320,7 +298,6 @@ async fn from_non_existent_branch() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// branch should be created unapplied, because of the conflict
|
||||
@ -332,15 +309,14 @@ async fn from_non_existent_branch() {
|
||||
&"refs/remotes/origin/branch".parse().unwrap(),
|
||||
None
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"branch refs/remotes/origin/branch was not found"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn from_state_remote_branch() {
|
||||
#[test]
|
||||
fn from_state_remote_branch() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -365,7 +341,6 @@ async fn from_state_remote_branch() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
@ -374,10 +349,9 @@ async fn from_state_remote_branch() {
|
||||
&"refs/remotes/origin/branch".parse().unwrap(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
|
@ -1,8 +1,9 @@
|
||||
use super::*;
|
||||
use gitbutler_branch::BranchCreateRequest;
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_unapply_diff() {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_unapply_diff() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -12,20 +13,18 @@ async fn should_unapply_diff() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// write some
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
|
||||
controller
|
||||
.delete_virtual_branch(project, branches[0].id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
assert!(!repository.path().join("file.txt").exists());
|
||||
|
||||
@ -37,8 +36,8 @@ async fn should_unapply_diff() {
|
||||
assert!(!refnames.contains(&"refs/gitbutler/name".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_remove_reference() {
|
||||
#[test]
|
||||
fn should_remove_reference() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -48,7 +47,6 @@ async fn should_remove_reference() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = controller
|
||||
@ -59,12 +57,11 @@ async fn should_remove_reference() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller.delete_virtual_branch(project, id).await.unwrap();
|
||||
controller.delete_virtual_branch(project, id).unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
|
||||
let refnames = repository
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn twice() {
|
||||
#[test]
|
||||
fn twice() {
|
||||
let data_dir = paths::data_dir();
|
||||
let projects = projects::Controller::from_path(data_dir.path());
|
||||
|
||||
@ -15,40 +15,33 @@ async fn twice() {
|
||||
.expect("failed to add project");
|
||||
controller
|
||||
.set_base_branch(&project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(controller
|
||||
.list_virtual_branches(&project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.is_empty());
|
||||
projects.delete(project.id).await.unwrap();
|
||||
controller
|
||||
.list_virtual_branches(&project)
|
||||
.await
|
||||
.unwrap_err();
|
||||
projects.delete(project.id).unwrap();
|
||||
controller.list_virtual_branches(&project).unwrap_err();
|
||||
}
|
||||
|
||||
{
|
||||
let project = projects.add(test_project.path()).unwrap();
|
||||
controller
|
||||
.set_base_branch(&project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// even though project is on gitbutler/integration, we should not import it
|
||||
assert!(controller
|
||||
.list_virtual_branches(&project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dirty_non_target() {
|
||||
#[test]
|
||||
fn dirty_non_target() {
|
||||
// a situation when you initialize project while being on the local verison of the master
|
||||
// that has uncommited changes.
|
||||
let Test {
|
||||
@ -64,10 +57,9 @@ async fn dirty_non_target() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
assert_eq!(branches[0].files[0].hunks.len(), 1);
|
||||
@ -75,8 +67,8 @@ async fn dirty_non_target() {
|
||||
assert_eq!(branches[0].name, "some-feature");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dirty_target() {
|
||||
#[test]
|
||||
fn dirty_target() {
|
||||
// a situation when you initialize project while being on the local verison of the master
|
||||
// that has uncommited changes.
|
||||
let Test {
|
||||
@ -90,10 +82,9 @@ async fn dirty_target() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
assert_eq!(branches[0].files[0].hunks.len(), 1);
|
||||
@ -101,8 +92,8 @@ async fn dirty_target() {
|
||||
assert_eq!(branches[0].name, "master");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn commit_on_non_target_local() {
|
||||
#[test]
|
||||
fn commit_on_non_target_local() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -116,10 +107,9 @@ async fn commit_on_non_target_local() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert!(branches[0].files.is_empty());
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -127,8 +117,8 @@ async fn commit_on_non_target_local() {
|
||||
assert_eq!(branches[0].name, "some-feature");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn commit_on_non_target_remote() {
|
||||
#[test]
|
||||
fn commit_on_non_target_remote() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -143,10 +133,9 @@ async fn commit_on_non_target_remote() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert!(branches[0].files.is_empty());
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -154,8 +143,8 @@ async fn commit_on_non_target_remote() {
|
||||
assert_eq!(branches[0].name, "some-feature");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn commit_on_target() {
|
||||
#[test]
|
||||
fn commit_on_target() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -168,10 +157,9 @@ async fn commit_on_target() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert!(branches[0].files.is_empty());
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -179,8 +167,8 @@ async fn commit_on_target() {
|
||||
assert_eq!(branches[0].name, "master");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn submodule() {
|
||||
#[test]
|
||||
fn submodule() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -195,10 +183,9 @@ async fn submodule() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
assert_eq!(branches[0].files[0].hunks.len(), 1);
|
||||
|
@ -1,8 +1,9 @@
|
||||
use super::*;
|
||||
use gitbutler_branch::BranchCreateRequest;
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_blank_commit_down() {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn insert_blank_commit_down() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -12,19 +13,16 @@ async fn insert_blank_commit_down() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
@ -32,24 +30,20 @@ async fn insert_blank_commit_down() {
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file4.txt"), "content4").unwrap();
|
||||
let _commit3_id = controller
|
||||
.create_commit(project, branch_id, "commit three", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.insert_blank_commit(project, branch_id, commit2_id, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -73,8 +67,8 @@ async fn insert_blank_commit_down() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_blank_commit_up() {
|
||||
#[test]
|
||||
fn insert_blank_commit_up() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -84,19 +78,16 @@ async fn insert_blank_commit_up() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
@ -104,24 +95,20 @@ async fn insert_blank_commit_up() {
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file4.txt"), "content4").unwrap();
|
||||
let _commit3_id = controller
|
||||
.create_commit(project, branch_id, "commit three", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.insert_blank_commit(project, branch_id, commit2_id, -1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
|
133
crates/gitbutler-branch-actions/tests/virtual_branches/list.rs
Normal file
133
crates/gitbutler-branch-actions/tests/virtual_branches/list.rs
Normal file
@ -0,0 +1,133 @@
|
||||
use anyhow::Result;
|
||||
use gitbutler_branch_actions::{list_branches, Author};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main", "short names are used");
|
||||
assert_eq!(branch.remotes, ["origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(
|
||||
branch.authors,
|
||||
[],
|
||||
"there is no local commit, so no authors are known"
|
||||
);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch_multiple_remotes() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch-multi-remote")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main");
|
||||
assert_eq!(branch.remotes, ["other-origin", "origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(branch.authors, []);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch_one_commit() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch-one-commit")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main");
|
||||
assert_eq!(branch.remotes, ["origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(
|
||||
branch.number_of_commits, 0,
|
||||
"local-only commits aren't detected"
|
||||
);
|
||||
assert_eq!(
|
||||
branch.authors,
|
||||
[],
|
||||
"and thus there is no ownership information"
|
||||
);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_vbranch_on_integration() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("one-vbranch-on-integration")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "virtual");
|
||||
assert!(branch.remotes.is_empty(), "no remote is associated yet");
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(
|
||||
branch
|
||||
.virtual_branch
|
||||
.as_ref()
|
||||
.map(|v| v.given_name.as_str()),
|
||||
Some("virtual")
|
||||
);
|
||||
assert_eq!(branch.authors, []);
|
||||
assert!(branch.own_branch, "zero commits means user owns the branch");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_vbranch_on_integration_one_commit() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("one-vbranch-on-integration-one-commit")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "virtual");
|
||||
assert!(branch.remotes.is_empty(), "no remote is associated yet");
|
||||
assert_eq!(
|
||||
branch
|
||||
.virtual_branch
|
||||
.as_ref()
|
||||
.map(|v| v.given_name.as_str()),
|
||||
Some("virtual")
|
||||
);
|
||||
assert_eq!(branch.number_of_commits, 1, "one commit created on vbranch");
|
||||
assert_eq!(branch.authors, [default_author()]);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function affects all tests, but those who care should just call it, assuming
|
||||
/// they all care for the same default value.
|
||||
/// If not, they should be placed in their own integration test or run with `#[serial_test:serial]`.
|
||||
fn init_env() {
|
||||
for (name, value) in [
|
||||
("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000"),
|
||||
("GIT_AUTHOR_EMAIL", "author@example.com"),
|
||||
("GIT_AUTHOR_NAME", "author"),
|
||||
("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000"),
|
||||
("GIT_COMMITTER_EMAIL", "committer@example.com"),
|
||||
("GIT_COMMITTER_NAME", "committer"),
|
||||
] {
|
||||
std::env::set_var(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_author() -> Author {
|
||||
Author {
|
||||
name: Some("author".into()),
|
||||
email: Some("author@example.com".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_ctx(name: &str) -> anyhow::Result<CommandContext> {
|
||||
gitbutler_testsupport::read_only::fixture("for-listing.sh", name)
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, path, str::FromStr};
|
||||
use std::{fs, path, path::PathBuf, str::FromStr};
|
||||
|
||||
use gitbutler_branch::BranchCreateRequest;
|
||||
use gitbutler_branch_actions::VirtualBranchActions;
|
||||
@ -65,6 +64,7 @@ mod create_virtual_branch_from_branch;
|
||||
mod delete_virtual_branch;
|
||||
mod init;
|
||||
mod insert_blank_commit;
|
||||
mod list;
|
||||
mod move_commit_file;
|
||||
mod move_commit_to_vbranch;
|
||||
mod oplog;
|
||||
@ -81,8 +81,8 @@ mod update_commit_message;
|
||||
mod upstream;
|
||||
mod verify_branch;
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_conflict_flow() {
|
||||
#[test]
|
||||
fn resolve_conflict_flow() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -104,18 +104,16 @@ async fn resolve_conflict_flow() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
// make a branch that conflicts with the remote branch, but doesn't know about it yet
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs::write(repository.path().join("file.txt"), "conflict").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert!(branches[0].active);
|
||||
@ -123,11 +121,11 @@ async fn resolve_conflict_flow() {
|
||||
|
||||
let unapplied_branch = {
|
||||
// fetch remote. There is now a conflict, so the branch will be unapplied
|
||||
let unapplied_branches = controller.update_base_branch(project).await.unwrap();
|
||||
let unapplied_branches = controller.update_base_branch(project).unwrap();
|
||||
assert_eq!(unapplied_branches.len(), 1);
|
||||
|
||||
// there is a conflict now, so the branch should be inactive
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 0);
|
||||
|
||||
Refname::from_str(&unapplied_branches[0]).unwrap()
|
||||
@ -137,10 +135,9 @@ async fn resolve_conflict_flow() {
|
||||
// when we apply conflicted branch, it has conflict
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert!(branches[0].active);
|
||||
assert!(branches[0].conflicted);
|
||||
@ -160,7 +157,6 @@ async fn resolve_conflict_flow() {
|
||||
assert!(matches!(
|
||||
controller
|
||||
.create_commit(project, branch1_id, "commit conflicts", None, false)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.downcast_ref(),
|
||||
Some(Marker::ProjectConflict)
|
||||
@ -170,16 +166,15 @@ async fn resolve_conflict_flow() {
|
||||
{
|
||||
// fixing the conflict removes conflicted mark
|
||||
fs::write(repository.path().join("file.txt"), "resolved").unwrap();
|
||||
controller.list_virtual_branches(project).await.unwrap();
|
||||
controller.list_virtual_branches(project).unwrap();
|
||||
let commit_oid = controller
|
||||
.create_commit(project, branch1_id, "resolution", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let commit = repository.find_commit(commit_oid).unwrap();
|
||||
assert_eq!(commit.parent_count(), 2);
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert!(branches[0].active);
|
||||
|
@ -1,11 +1,10 @@
|
||||
use gitbutler_branch::BranchCreateRequest;
|
||||
use gitbutler_branch::BranchOwnershipClaims;
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchOwnershipClaims};
|
||||
use gitbutler_commit::commit_ext::CommitExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn move_file_down() {
|
||||
#[test]
|
||||
fn move_file_down() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -15,19 +14,16 @@ async fn move_file_down() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let commit1 = repository.find_commit(commit1_id).unwrap();
|
||||
|
||||
@ -36,7 +32,6 @@ async fn move_file_down() {
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let commit2 = repository.find_commit(commit2_id).unwrap();
|
||||
|
||||
@ -44,12 +39,10 @@ async fn move_file_down() {
|
||||
let to_amend: BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.move_commit_file(project, branch_id, commit2_id, commit1_id, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -68,8 +61,8 @@ async fn move_file_down() {
|
||||
assert_eq!(branch.commits[1].files.len(), 2); // this now has both file changes
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn move_file_up() {
|
||||
#[test]
|
||||
fn move_file_up() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -79,12 +72,10 @@ async fn move_file_up() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
@ -92,26 +83,22 @@ async fn move_file_up() {
|
||||
fs::write(repository.path().join("file2.txt"), "content2").unwrap();
|
||||
let commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// amend another hunk
|
||||
let to_amend: BranchOwnershipClaims = "file2.txt:1-2".parse().unwrap();
|
||||
controller
|
||||
.move_commit_file(project, branch_id, commit1_id, commit2_id, &to_amend)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -127,8 +114,8 @@ async fn move_file_up() {
|
||||
// This is out of scope for the first release, but should be fixed in the future
|
||||
// where you can take overlapping hunks between commits and resolve a move between them
|
||||
/*
|
||||
#[tokio::test]
|
||||
async fn move_file_up_overlapping_hunks() {
|
||||
#[test]
|
||||
fn move_file_up_overlapping_hunks() {
|
||||
let Test {
|
||||
repository,
|
||||
project_id,
|
||||
@ -138,19 +125,19 @@ async fn move_file_up_overlapping_hunks() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
|
||||
.unwrap();
|
||||
|
||||
// create bottom commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
|
||||
.unwrap();
|
||||
|
||||
// create middle commit one
|
||||
@ -158,7 +145,7 @@ async fn move_file_up_overlapping_hunks() {
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await
|
||||
|
||||
.unwrap();
|
||||
|
||||
// create middle commit two
|
||||
@ -170,26 +157,26 @@ async fn move_file_up_overlapping_hunks() {
|
||||
fs::write(repository.path().join("file4.txt"), "content4").unwrap();
|
||||
let commit3_id = controller
|
||||
.create_commit(project, branch_id, "commit three", None, false)
|
||||
.await
|
||||
|
||||
.unwrap();
|
||||
|
||||
// create top commit
|
||||
fs::write(repository.path().join("file5.txt"), "content5").unwrap();
|
||||
let _commit4_id = controller
|
||||
.create_commit(project, branch_id, "commit four", None, false)
|
||||
.await
|
||||
|
||||
.unwrap();
|
||||
|
||||
// move one line from middle commit two up to middle commit one
|
||||
let to_amend: BranchOwnershipClaims = "file2.txt:1-6".parse().unwrap();
|
||||
controller
|
||||
.move_commit_file(project, branch_id, commit2_id, commit3_id, &to_amend)
|
||||
.await
|
||||
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
|
@ -2,8 +2,8 @@ use gitbutler_branch::{BranchCreateRequest, BranchId};
|
||||
|
||||
use super::Test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_diffs() {
|
||||
#[test]
|
||||
fn no_diffs() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -13,34 +13,29 @@ async fn no_diffs() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
let source_branch_id = branches[0].id;
|
||||
|
||||
let commit_oid = controller
|
||||
.create_commit(project, source_branch_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let target_branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.move_commit(project, target_branch_id, commit_oid)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let destination_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -49,7 +44,6 @@ async fn no_diffs() {
|
||||
|
||||
let source_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -62,8 +56,8 @@ async fn no_diffs() {
|
||||
assert_eq!(source_branch.files.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn diffs_on_source_branch() {
|
||||
#[test]
|
||||
fn diffs_on_source_branch() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -73,19 +67,17 @@ async fn diffs_on_source_branch() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
let source_branch_id = branches[0].id;
|
||||
|
||||
let commit_oid = controller
|
||||
.create_commit(project, source_branch_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(
|
||||
@ -96,17 +88,14 @@ async fn diffs_on_source_branch() {
|
||||
|
||||
let target_branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.move_commit(project, target_branch_id, commit_oid)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let destination_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -115,7 +104,6 @@ async fn diffs_on_source_branch() {
|
||||
|
||||
let source_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -128,8 +116,8 @@ async fn diffs_on_source_branch() {
|
||||
assert_eq!(source_branch.files.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn diffs_on_target_branch() {
|
||||
#[test]
|
||||
fn diffs_on_target_branch() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -139,19 +127,17 @@ async fn diffs_on_target_branch() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
let source_branch_id = branches[0].id;
|
||||
|
||||
let commit_oid = controller
|
||||
.create_commit(project, source_branch_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let target_branch_id = controller
|
||||
@ -162,7 +148,6 @@ async fn diffs_on_target_branch() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(
|
||||
@ -173,12 +158,10 @@ async fn diffs_on_target_branch() {
|
||||
|
||||
controller
|
||||
.move_commit(project, target_branch_id, commit_oid)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let destination_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -187,7 +170,6 @@ async fn diffs_on_target_branch() {
|
||||
|
||||
let source_branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -200,8 +182,8 @@ async fn diffs_on_target_branch() {
|
||||
assert_eq!(source_branch.files.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn locked_hunks_on_source_branch() {
|
||||
#[test]
|
||||
fn locked_hunks_on_source_branch() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -211,40 +193,36 @@ async fn locked_hunks_on_source_branch() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
let source_branch_id = branches[0].id;
|
||||
|
||||
let commit_oid = controller
|
||||
.create_commit(project, source_branch_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "locked content").unwrap();
|
||||
|
||||
let target_branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
controller
|
||||
.move_commit(project, target_branch_id, commit_oid)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"the source branch contains hunks locked to the target commit"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_commit() {
|
||||
#[test]
|
||||
fn no_commit() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -254,24 +232,21 @@ async fn no_commit() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
let source_branch_id = branches[0].id;
|
||||
|
||||
controller
|
||||
.create_commit(project, source_branch_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let target_branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let commit_id_hex = "a99c95cca7a60f1a2180c2f86fb18af97333c192";
|
||||
@ -282,15 +257,14 @@ async fn no_commit() {
|
||||
target_branch_id,
|
||||
git2::Oid::from_str(commit_id_hex).unwrap()
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
format!("commit {commit_id_hex} to be moved could not be found")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_branch() {
|
||||
#[test]
|
||||
fn no_branch() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -300,26 +274,23 @@ async fn no_branch() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
let source_branch_id = branches[0].id;
|
||||
|
||||
let commit_oid = controller
|
||||
.create_commit(project, source_branch_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = BranchId::generate();
|
||||
assert_eq!(
|
||||
controller
|
||||
.move_commit(project, id, commit_oid)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
format!("branch {id} is not among applied branches")
|
||||
|
@ -1,13 +1,13 @@
|
||||
use super::*;
|
||||
use std::{io::Write, path::Path, time::Duration};
|
||||
|
||||
use gitbutler_branch::{BranchCreateRequest, VirtualBranchesHandle};
|
||||
use gitbutler_oplog::OplogExt;
|
||||
use itertools::Itertools;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn workdir_vbranch_restore() -> anyhow::Result<()> {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn workdir_vbranch_restore() -> anyhow::Result<()> {
|
||||
let test = Test::default();
|
||||
let Test {
|
||||
repository,
|
||||
@ -18,7 +18,6 @@ async fn workdir_vbranch_restore() -> anyhow::Result<()> {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let worktree_dir = repository.path();
|
||||
@ -28,24 +27,20 @@ async fn workdir_vbranch_restore() -> anyhow::Result<()> {
|
||||
worktree_dir.join(format!("file{round}.txt")),
|
||||
make_lines(line_count),
|
||||
)?;
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(
|
||||
project,
|
||||
&BranchCreateRequest {
|
||||
name: Some(round.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
controller
|
||||
.create_commit(
|
||||
project,
|
||||
branch_id,
|
||||
&format!("commit {round}"),
|
||||
None,
|
||||
false, /* run hook */
|
||||
)
|
||||
.await?;
|
||||
let branch_id = controller.create_virtual_branch(
|
||||
project,
|
||||
&BranchCreateRequest {
|
||||
name: Some(round.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
controller.create_commit(
|
||||
project,
|
||||
branch_id,
|
||||
&format!("commit {round}"),
|
||||
None,
|
||||
false, /* run hook */
|
||||
)?;
|
||||
assert_eq!(
|
||||
wd_file_count(&worktree_dir)?,
|
||||
round + 1,
|
||||
@ -56,9 +51,7 @@ async fn workdir_vbranch_restore() -> anyhow::Result<()> {
|
||||
line_count > 20
|
||||
);
|
||||
}
|
||||
let _empty = controller
|
||||
.create_virtual_branch(project, &Default::default())
|
||||
.await?;
|
||||
let _empty = controller.create_virtual_branch(project, &Default::default())?;
|
||||
|
||||
let snapshots = project.list_snapshots(10, None)?;
|
||||
assert_eq!(
|
||||
@ -99,8 +92,8 @@ fn make_lines(count: usize) -> Vec<u8> {
|
||||
(0..count).map(|n| n.to_string()).join("\n").into()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_oplog() -> anyhow::Result<()> {
|
||||
#[test]
|
||||
fn basic_oplog() -> anyhow::Result<()> {
|
||||
let Test {
|
||||
repository,
|
||||
controller,
|
||||
@ -108,19 +101,13 @@ async fn basic_oplog() -> anyhow::Result<()> {
|
||||
..
|
||||
} = &Test::default();
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse()?)
|
||||
.await?;
|
||||
controller.set_base_branch(project, &"refs/remotes/origin/master".parse()?)?;
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await?;
|
||||
let branch_id = controller.create_virtual_branch(project, &BranchCreateRequest::default())?;
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content")?;
|
||||
let _commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await?;
|
||||
let _commit1_id = controller.create_commit(project, branch_id, "commit one", None, false)?;
|
||||
|
||||
// dont store large files
|
||||
let file_path = repository.path().join("large.txt");
|
||||
@ -134,9 +121,7 @@ async fn basic_oplog() -> anyhow::Result<()> {
|
||||
// create commit with large file
|
||||
fs::write(repository.path().join("file2.txt"), "content2")?;
|
||||
fs::write(repository.path().join("file3.txt"), "content3")?;
|
||||
let commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await?;
|
||||
let commit2_id = controller.create_commit(project, branch_id, "commit two", None, false)?;
|
||||
|
||||
// Create conflict state
|
||||
let conflicts_path = repository.path().join(".git").join("conflicts");
|
||||
@ -145,27 +130,23 @@ async fn basic_oplog() -> anyhow::Result<()> {
|
||||
std::fs::write(&base_merge_parent_path, "parent A")?;
|
||||
|
||||
// create state with conflict state
|
||||
let _empty_branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await?;
|
||||
let _empty_branch_id =
|
||||
controller.create_virtual_branch(project, &BranchCreateRequest::default())?;
|
||||
|
||||
std::fs::remove_file(&base_merge_parent_path)?;
|
||||
std::fs::remove_file(&conflicts_path)?;
|
||||
|
||||
fs::write(repository.path().join("file4.txt"), "content4")?;
|
||||
let _commit3_id = controller
|
||||
.create_commit(project, branch_id, "commit three", None, false)
|
||||
.await?;
|
||||
let _commit3_id = controller.create_commit(project, branch_id, "commit three", None, false)?;
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await?
|
||||
.list_virtual_branches(project)?
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|b| b.id == branch_id)
|
||||
.unwrap();
|
||||
|
||||
let branches = controller.list_virtual_branches(project).await?;
|
||||
let branches = controller.list_virtual_branches(project)?;
|
||||
assert_eq!(branches.0.len(), 2);
|
||||
|
||||
assert_eq!(branch.commits.len(), 3);
|
||||
@ -204,7 +185,7 @@ async fn basic_oplog() -> anyhow::Result<()> {
|
||||
project.restore_snapshot(snapshots[2].clone().commit_id)?;
|
||||
|
||||
// the restore removed our new branch
|
||||
let branches = controller.list_virtual_branches(project).await?;
|
||||
let branches = controller.list_virtual_branches(project)?;
|
||||
assert_eq!(branches.0.len(), 1);
|
||||
|
||||
// assert that the conflicts file was removed
|
||||
@ -248,8 +229,8 @@ async fn basic_oplog() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
||||
#[test]
|
||||
fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
||||
let Test {
|
||||
repository,
|
||||
controller,
|
||||
@ -257,9 +238,7 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
||||
..
|
||||
} = &Test::default();
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse()?)
|
||||
.await?;
|
||||
controller.set_base_branch(project, &"refs/remotes/origin/master".parse()?)?;
|
||||
|
||||
assert_eq!(
|
||||
VirtualBranchesHandle::new(project.gb_dir())
|
||||
@ -267,9 +246,7 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await?;
|
||||
let branch_id = controller.create_virtual_branch(project, &BranchCreateRequest::default())?;
|
||||
assert_eq!(
|
||||
VirtualBranchesHandle::new(project.gb_dir())
|
||||
.list_branches_in_workspace()?
|
||||
@ -279,9 +256,7 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content")?;
|
||||
let _commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await?;
|
||||
let _commit1_id = controller.create_commit(project, branch_id, "commit one", None, false)?;
|
||||
|
||||
let repo = git2::Repository::open(&project.path)?;
|
||||
|
||||
@ -294,9 +269,7 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
||||
|
||||
// create second commit
|
||||
fs::write(repository.path().join("file.txt"), "changed content")?;
|
||||
let _commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await?;
|
||||
let _commit2_id = controller.create_commit(project, branch_id, "commit two", None, false)?;
|
||||
|
||||
// check the integration commit changed
|
||||
let head = repo.head().expect("never unborn");
|
||||
@ -370,8 +343,8 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
// test operations-log.toml head is not a commit
|
||||
#[tokio::test]
|
||||
async fn head_corrupt_is_recreated_automatically() {
|
||||
#[test]
|
||||
fn head_corrupt_is_recreated_automatically() {
|
||||
let Test {
|
||||
repository,
|
||||
controller,
|
||||
@ -381,11 +354,9 @@ async fn head_corrupt_is_recreated_automatically() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let snapshots = project.list_snapshots(10, None).unwrap();
|
||||
@ -405,7 +376,6 @@ async fn head_corrupt_is_recreated_automatically() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.expect("the snapshot doesn't fail despite the corrupt head");
|
||||
|
||||
let snapshots = project.list_snapshots(10, None).unwrap();
|
||||
|
@ -1,11 +1,12 @@
|
||||
use super::*;
|
||||
|
||||
mod create_virtual_branch {
|
||||
use super::*;
|
||||
use gitbutler_branch::BranchCreateRequest;
|
||||
|
||||
#[tokio::test]
|
||||
async fn simple() {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -15,15 +16,13 @@ mod create_virtual_branch {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch_id);
|
||||
assert_eq!(branches[0].name, "Virtual branch");
|
||||
@ -36,8 +35,8 @@ mod create_virtual_branch {
|
||||
assert!(refnames.contains(&"refs/gitbutler/Virtual-branch".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn duplicate_name() {
|
||||
#[test]
|
||||
fn duplicate_name() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -47,7 +46,6 @@ mod create_virtual_branch {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = controller
|
||||
@ -58,7 +56,6 @@ mod create_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch2_id = controller
|
||||
@ -69,10 +66,9 @@ mod create_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 2);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].name, "name");
|
||||
@ -90,11 +86,12 @@ mod create_virtual_branch {
|
||||
}
|
||||
|
||||
mod update_virtual_branch {
|
||||
use super::*;
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
|
||||
|
||||
#[tokio::test]
|
||||
async fn simple() {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -104,7 +101,6 @@ mod update_virtual_branch {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
@ -115,7 +111,6 @@ mod update_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
@ -127,10 +122,9 @@ mod update_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch_id);
|
||||
assert_eq!(branches[0].name, "new name");
|
||||
@ -144,8 +138,8 @@ mod update_virtual_branch {
|
||||
assert!(refnames.contains(&"refs/gitbutler/new-name".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn duplicate_name() {
|
||||
#[test]
|
||||
fn duplicate_name() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -155,7 +149,6 @@ mod update_virtual_branch {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = controller
|
||||
@ -166,7 +159,6 @@ mod update_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch2_id = controller
|
||||
@ -176,7 +168,6 @@ mod update_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
@ -188,10 +179,9 @@ mod update_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 2);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].name, "name");
|
||||
@ -209,11 +199,12 @@ mod update_virtual_branch {
|
||||
}
|
||||
|
||||
mod push_virtual_branch {
|
||||
use super::*;
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
|
||||
|
||||
#[tokio::test]
|
||||
async fn simple() {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -223,7 +214,6 @@ mod push_virtual_branch {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = controller
|
||||
@ -234,21 +224,18 @@ mod push_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
controller
|
||||
.create_commit(project, branch1_id, "test", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
controller
|
||||
.push_virtual_branch(project, branch1_id, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].name, "name");
|
||||
@ -265,8 +252,8 @@ mod push_virtual_branch {
|
||||
assert!(refnames.contains(&branches[0].upstream.clone().unwrap().name.to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn duplicate_names() {
|
||||
#[test]
|
||||
fn duplicate_names() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -276,7 +263,6 @@ mod push_virtual_branch {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = {
|
||||
@ -289,16 +275,13 @@ mod push_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
controller
|
||||
.create_commit(project, branch1_id, "test", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
controller
|
||||
.push_virtual_branch(project, branch1_id, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
branch1_id
|
||||
};
|
||||
@ -313,7 +296,6 @@ mod push_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch2_id = {
|
||||
@ -326,21 +308,18 @@ mod push_virtual_branch {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
fs::write(repository.path().join("file.txt"), "updated content").unwrap();
|
||||
controller
|
||||
.create_commit(project, branch2_id, "test", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
controller
|
||||
.push_virtual_branch(project, branch2_id, false, None)
|
||||
.await
|
||||
.unwrap();
|
||||
branch2_id
|
||||
};
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 2);
|
||||
// first branch is pushing to old ref remotely
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
|
@ -1,8 +1,9 @@
|
||||
use super::*;
|
||||
use gitbutler_branch::BranchCreateRequest;
|
||||
|
||||
#[tokio::test]
|
||||
async fn reorder_commit_down() {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reorder_commit_down() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -12,19 +13,16 @@ async fn reorder_commit_down() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let _commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
@ -32,17 +30,14 @@ async fn reorder_commit_down() {
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.reorder_commit(project, branch_id, commit2_id, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -62,8 +57,8 @@ async fn reorder_commit_down() {
|
||||
assert_eq!(descriptions, vec!["commit one", "commit two"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reorder_commit_up() {
|
||||
#[test]
|
||||
fn reorder_commit_up() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -73,19 +68,16 @@ async fn reorder_commit_up() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
let commit1_id = controller
|
||||
.create_commit(project, branch_id, "commit one", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create commit
|
||||
@ -93,17 +85,14 @@ async fn reorder_commit_up() {
|
||||
fs::write(repository.path().join("file3.txt"), "content3").unwrap();
|
||||
let _commit2_id = controller
|
||||
.create_commit(project, branch_id, "commit two", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
controller
|
||||
.reorder_commit(project, branch_id, commit1_id, -1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
|
@ -4,8 +4,8 @@ use gitbutler_branch::BranchCreateRequest;
|
||||
|
||||
use super::Test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn to_head() {
|
||||
#[test]
|
||||
fn to_head() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -15,12 +15,10 @@ async fn to_head() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let oid = {
|
||||
@ -29,10 +27,9 @@ async fn to_head() {
|
||||
// commit changes
|
||||
let oid = controller
|
||||
.create_commit(project, branch1_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -50,10 +47,9 @@ async fn to_head() {
|
||||
// reset changes to head
|
||||
controller
|
||||
.reset_virtual_branch(project, branch1_id, oid)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -66,8 +62,8 @@ async fn to_head() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn to_target() {
|
||||
#[test]
|
||||
fn to_target() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -77,12 +73,10 @@ async fn to_target() {
|
||||
|
||||
let base_branch = controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
@ -91,10 +85,9 @@ async fn to_target() {
|
||||
// commit changes
|
||||
let oid = controller
|
||||
.create_commit(project, branch1_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -110,10 +103,9 @@ async fn to_target() {
|
||||
// reset changes to head
|
||||
controller
|
||||
.reset_virtual_branch(project, branch1_id, base_branch.base_sha)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 0);
|
||||
@ -125,8 +117,8 @@ async fn to_target() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn to_commit() {
|
||||
#[test]
|
||||
fn to_commit() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -136,12 +128,10 @@ async fn to_commit() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first_commit_oid = {
|
||||
@ -151,10 +141,9 @@ async fn to_commit() {
|
||||
|
||||
let oid = controller
|
||||
.create_commit(project, branch1_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -174,10 +163,9 @@ async fn to_commit() {
|
||||
|
||||
let second_commit_oid = controller
|
||||
.create_commit(project, branch1_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 2);
|
||||
@ -194,10 +182,9 @@ async fn to_commit() {
|
||||
// reset changes to the first commit
|
||||
controller
|
||||
.reset_virtual_branch(project, branch1_id, first_commit_oid)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -210,8 +197,8 @@ async fn to_commit() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn to_non_existing() {
|
||||
#[test]
|
||||
fn to_non_existing() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -221,12 +208,10 @@ async fn to_non_existing() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let branch1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
@ -235,10 +220,9 @@ async fn to_non_existing() {
|
||||
// commit changes
|
||||
let oid = controller
|
||||
.create_commit(project, branch1_id, "commit", None, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, branch1_id);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
@ -259,7 +243,6 @@ async fn to_non_existing() {
|
||||
branch1_id,
|
||||
"fe14df8c66b73c6276f7bb26102ad91da680afcb".parse().unwrap()
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"commit fe14df8c66b73c6276f7bb26102ad91da680afcb not in the branch"
|
||||
|
@ -1,8 +1,9 @@
|
||||
use super::*;
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchUpdateRequest};
|
||||
|
||||
#[tokio::test]
|
||||
async fn unapplying_selected_branch_selects_anther() {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unapplying_selected_branch_selects_anther() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -12,7 +13,6 @@ async fn unapplying_selected_branch_selects_anther() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file one.txt"), "").unwrap();
|
||||
@ -20,16 +20,14 @@ async fn unapplying_selected_branch_selects_anther() {
|
||||
// first branch should be created as default
|
||||
let b_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// if default branch exists, new branch should not be created as default
|
||||
let b2_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
|
||||
let b = branches.iter().find(|b| b.id == b_id).unwrap();
|
||||
|
||||
@ -38,12 +36,9 @@ async fn unapplying_selected_branch_selects_anther() {
|
||||
assert!(b.selected_for_changes);
|
||||
assert!(!b2.selected_for_changes);
|
||||
|
||||
controller
|
||||
.convert_to_real_branch(project, b_id)
|
||||
.await
|
||||
.unwrap();
|
||||
controller.convert_to_real_branch(project, b_id).unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, b2.id);
|
||||
@ -51,8 +46,8 @@ async fn unapplying_selected_branch_selects_anther() {
|
||||
assert!(branches[0].active);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_selected_branch_selects_anther() {
|
||||
#[test]
|
||||
fn deleting_selected_branch_selects_anther() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -61,22 +56,19 @@ async fn deleting_selected_branch_selects_anther() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// first branch should be created as default
|
||||
let b_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// if default branch exists, new branch should not be created as default
|
||||
let b2_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
|
||||
let b = branches.iter().find(|b| b.id == b_id).unwrap();
|
||||
|
||||
@ -85,20 +77,17 @@ async fn deleting_selected_branch_selects_anther() {
|
||||
assert!(b.selected_for_changes);
|
||||
assert!(!b2.selected_for_changes);
|
||||
|
||||
controller
|
||||
.delete_virtual_branch(project, b_id)
|
||||
.await
|
||||
.unwrap();
|
||||
controller.delete_virtual_branch(project, b_id).unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert_eq!(branches[0].id, b2.id);
|
||||
assert!(branches[0].selected_for_changes);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_virtual_branch_should_set_selected_for_changes() {
|
||||
#[test]
|
||||
fn create_virtual_branch_should_set_selected_for_changes() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -107,17 +96,14 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// first branch should be created as default
|
||||
let b_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -128,11 +114,9 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
|
||||
// if default branch exists, new branch should not be created as default
|
||||
let b_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -149,11 +133,9 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -170,11 +152,9 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let branch = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -183,8 +163,8 @@ async fn create_virtual_branch_should_set_selected_for_changes() {
|
||||
assert!(branch.selected_for_changes);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_virtual_branch_should_reset_selected_for_changes() {
|
||||
#[test]
|
||||
fn update_virtual_branch_should_reset_selected_for_changes() {
|
||||
let Test {
|
||||
project,
|
||||
controller,
|
||||
@ -193,16 +173,13 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let b1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let b1 = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -212,11 +189,9 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
|
||||
|
||||
let b2_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let b2 = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -233,12 +208,10 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let b1 = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -248,7 +221,6 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
|
||||
|
||||
let b2 = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -257,8 +229,8 @@ async fn update_virtual_branch_should_reset_selected_for_changes() {
|
||||
assert!(b2.selected_for_changes);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unapply_virtual_branch_should_reset_selected_for_changes() {
|
||||
#[test]
|
||||
fn unapply_virtual_branch_should_reset_selected_for_changes() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -268,18 +240,15 @@ async fn unapply_virtual_branch_should_reset_selected_for_changes() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let b1_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let b1 = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -289,12 +258,10 @@ async fn unapply_virtual_branch_should_reset_selected_for_changes() {
|
||||
|
||||
let b2_id = controller
|
||||
.create_virtual_branch(project, &BranchCreateRequest::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let b2 = controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
@ -302,22 +269,18 @@ async fn unapply_virtual_branch_should_reset_selected_for_changes() {
|
||||
.unwrap();
|
||||
assert!(!b2.selected_for_changes);
|
||||
|
||||
controller
|
||||
.convert_to_real_branch(project, b1_id)
|
||||
.await
|
||||
.unwrap();
|
||||
controller.convert_to_real_branch(project, b1_id).unwrap();
|
||||
|
||||
assert!(controller
|
||||
.list_virtual_branches(project)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.into_iter()
|
||||
.any(|b| b.selected_for_changes && b.id != b1_id))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hunks_distribution() {
|
||||
#[test]
|
||||
fn hunks_distribution() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -327,12 +290,11 @@ async fn hunks_distribution() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
|
||||
controller
|
||||
@ -343,16 +305,15 @@ async fn hunks_distribution() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
std::fs::write(repository.path().join("another_file.txt"), "content").unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
assert_eq!(branches[1].files.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn applying_first_branch() {
|
||||
#[test]
|
||||
fn applying_first_branch() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -362,25 +323,22 @@ async fn applying_first_branch() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(repository.path().join("file.txt"), "content").unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
|
||||
let unapplied_branch = controller
|
||||
.convert_to_real_branch(project, branches[0].id)
|
||||
.await
|
||||
.unwrap();
|
||||
let unapplied_branch = Refname::from_str(&unapplied_branch).unwrap();
|
||||
controller
|
||||
.create_virtual_branch_from_branch(project, &unapplied_branch, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches.len(), 1);
|
||||
assert!(branches[0].active);
|
||||
assert!(branches[0].selected_for_changes);
|
||||
@ -388,8 +346,8 @@ async fn applying_first_branch() {
|
||||
|
||||
// This test was written in response to issue #4148, to ensure the appearence
|
||||
// of a locked hunk doesn't drag along unrelated hunks to its branch.
|
||||
#[tokio::test]
|
||||
async fn new_locked_hunk_without_modifying_existing() {
|
||||
#[test]
|
||||
fn new_locked_hunk_without_modifying_existing() {
|
||||
let Test {
|
||||
repository,
|
||||
project,
|
||||
@ -403,21 +361,19 @@ async fn new_locked_hunk_without_modifying_existing() {
|
||||
|
||||
controller
|
||||
.set_base_branch(project, &"refs/remotes/origin/master".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
lines[0] = "modification 1".to_string();
|
||||
repository.write_file("file.txt", &lines);
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
|
||||
controller
|
||||
.create_commit(project, branches[0].id, "second commit", None, false)
|
||||
.await
|
||||
.expect("failed to create commit");
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches[0].files.len(), 0);
|
||||
assert_eq!(branches[0].commits.len(), 1);
|
||||
|
||||
@ -429,19 +385,18 @@ async fn new_locked_hunk_without_modifying_existing() {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
lines[8] = "modification 2".to_string();
|
||||
repository.write_file("file.txt", &lines);
|
||||
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches[0].files.len(), 0);
|
||||
assert_eq!(branches[1].files.len(), 1);
|
||||
|
||||
lines[0] = "modification 3".to_string();
|
||||
repository.write_file("file.txt", &lines);
|
||||
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
|
||||
let (branches, _) = controller.list_virtual_branches(project).unwrap();
|
||||
assert_eq!(branches[0].files.len(), 1);
|
||||
assert_eq!(branches[1].files.len(), 1);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user