Merged origin/master into sidebar-branches

This commit is contained in:
Pavel Laptev 2024-07-29 14:24:48 +02:00 committed by GitButler
commit 2e3e733ac5
249 changed files with 5132 additions and 5611 deletions

View File

@ -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

View File

@ -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: |

View File

@ -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/**/*

View File

@ -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) }}

View File

@ -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
View File

@ -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

View File

@ -29,5 +29,8 @@
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[mdx]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

198
Cargo.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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,

View File

@ -1,5 +1,5 @@
{
"name": "@gitbutler/app",
"name": "@gitbutler/desktop",
"private": true,
"version": "0.0.0",
"type": "module",

View File

@ -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');
}
}

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -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();

View File

@ -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;
}

View 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>

View File

@ -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);

View File

@ -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}

View File

@ -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 {

View File

@ -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)}

View File

@ -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>

View File

@ -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 {

View File

@ -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">

View File

@ -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>

View 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;
}
}

View 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}`;
}
}

View 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;
}
}

View 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}`;
}
}

View File

@ -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);
});
});

View File

@ -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);
}
}
}

View 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}$`));
});
});

View File

@ -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}`;
}
}

View 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}$`));
});
});

View 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}`;
}
}

View File

@ -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.

View File

@ -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);

View File

@ -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);
});

View File

@ -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);
});
});

View 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;
}
}

View 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}`;
}
}

View File

@ -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>(

View File

@ -0,0 +1,3 @@
export interface GitHostBranch {
url: string;
}

View File

@ -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>;
}

View File

@ -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);

View File

@ -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}

View File

@ -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

View File

@ -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
};
}

View File

@ -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);

View File

@ -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);

View File

@ -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';

View File

@ -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,

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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",

View File

@ -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"

View File

@ -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)
}

View File

@ -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()

View File

@ -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)]

View File

@ -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)
}

View File

@ -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())?;

View File

@ -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 }
}
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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));

View File

@ -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

View File

@ -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();

View File

@ -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,
};

View File

@ -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")?;

View File

@ -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

View 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"
)

View File

@ -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"

View File

@ -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

View File

@ -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);
}

View File

@ -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()

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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()

View 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)
}

View File

@ -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);

View File

@ -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()

View File

@ -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")

View File

@ -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();

View File

@ -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);

View File

@ -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()

View File

@ -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"

View File

@ -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