Merge branch 'master' into extract-project-from-core-into-its-onw-crate

This commit is contained in:
Kiril Videlov 2024-07-08 17:57:17 +02:00 committed by GitHub
commit ba5b9ff0ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 456 additions and 411 deletions

View File

@ -1,15 +1,11 @@
export type ClickOpts = { trigger?: HTMLElement; handler: () => void; enabled?: boolean };
export type ClickOpts = { excludeElement?: HTMLElement; handler: () => void };
export function clickOutside(
node: HTMLElement,
params: ClickOpts
): { destroy: () => void; update: (opts: ClickOpts) => void } {
let trigger: HTMLElement | undefined;
export function clickOutside(node: HTMLElement, params: ClickOpts) {
function onClick(event: MouseEvent) {
if (
node &&
!node.contains(event.target as HTMLElement) &&
!trigger?.contains(event.target as HTMLElement)
!params.excludeElement?.contains(event.target as HTMLElement)
) {
params.handler();
}
@ -20,14 +16,6 @@ export function clickOutside(
destroy() {
document.removeEventListener('mousedown', onClick, true);
document.removeEventListener('contextmenu', onClick, true);
},
update(opts: ClickOpts) {
document.removeEventListener('mousedown', onClick, true);
document.removeEventListener('contextmenu', onClick, true);
if (opts.enabled !== undefined && !opts.enabled) return;
trigger = opts.trigger;
document.addEventListener('mousedown', onClick, true);
document.addEventListener('contextmenu', onClick, true);
}
};
}

View File

@ -1,21 +0,0 @@
export type ClickOpts = { excludeElement?: HTMLElement; handler: () => void };
export function clickOutside(node: HTMLElement, params: ClickOpts) {
function onClick(event: MouseEvent) {
if (
node &&
!node.contains(event.target as HTMLElement) &&
!params.excludeElement?.contains(event.target as HTMLElement)
) {
params.handler();
}
}
document.addEventListener('mousedown', onClick, true);
document.addEventListener('contextmenu', onClick, true);
return {
destroy() {
document.removeEventListener('mousedown', onClick, true);
document.removeEventListener('contextmenu', onClick, true);
}
};
}

View File

@ -1,7 +1,6 @@
<script lang="ts">
import { clickOutside } from '$lib/clickOutsideNew';
import { clickOutside } from '$lib/clickOutside';
import { portal } from '$lib/utils/portal';
import { pxToRem } from '$lib/utils/pxToRem';
import { resizeObserver } from '$lib/utils/resizeObserver';
import { type Snippet } from 'svelte';
@ -27,7 +26,6 @@
}: Props = $props();
// LOCAL VARS
let menuMargin = 4;
// STATES
let item = $state<any>();
@ -71,12 +69,10 @@
function setVerticalAlign(targetBoundingRect: DOMRect) {
if (verticalAlign === 'top') {
return targetBoundingRect?.top ? targetBoundingRect.top - contextMenuHeight - menuMargin : 0;
return targetBoundingRect?.top ? targetBoundingRect.top - contextMenuHeight : 0;
}
return targetBoundingRect?.top
? targetBoundingRect.top + targetBoundingRect.height + menuMargin
: 0;
return targetBoundingRect?.top ? targetBoundingRect.top + targetBoundingRect.height : 0;
}
function setHorizontalAlign(targetBoundingRect: DOMRect) {
@ -131,8 +127,10 @@
bind:offsetHeight={contextMenuHeight}
bind:offsetWidth={contextMenuWidth}
class="context-menu"
style:top={pxToRem(menuPosition.y)}
style:left={pxToRem(menuPosition.x)}
class:top-oriented={verticalAlign === 'top'}
class:bottom-oriented={verticalAlign === 'bottom'}
style:top="{menuPosition.y}px"
style:left="{menuPosition.x}px"
style:transform-origin={setTransformOrigin()}
style:--animation-transform-shift={verticalAlign === 'top' ? '6px' : '-6px'}
>
@ -167,6 +165,14 @@
/* background-color: rgba(0, 0, 0, 0.1); */
}
.top-oriented {
margin-top: -4px;
}
.bottom-oriented {
margin-top: 4px;
}
.context-menu {
z-index: var(--z-blocker);
position: fixed;

View File

@ -6,7 +6,7 @@
import ScrollableContainer from '../shared/ScrollableContainer.svelte';
import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw';
import { Project } from '$lib/backend/projects';
import { clickOutside } from '$lib/clickOutsideNew';
import { clickOutside } from '$lib/clickOutside';
import FileCard from '$lib/file/FileCard.svelte';
import SnapshotCard from '$lib/history/SnapshotCard.svelte';
import { HistoryService, createdOnDay } from '$lib/history/history';

View File

@ -85,14 +85,6 @@
class:resizer-hovered={isResizerHovered || isResizerDragging}
on:mousedown={toggleNavCollapse}
>
<!-- <svg viewBox="0 0 7 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6 1L1.81892 9.78026C1.30084 10.8682 1.30084 12.1318 1.81892 13.2197L6 22"
stroke-width="1.5"
vector-effect="non-scaling-stroke"
/>
</svg> -->
<svg viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 1.25L1.59055 5.08564C1.25376 5.46452 1.25376 6.03548 1.59055 6.41436L5 10.25"

View File

@ -2,35 +2,26 @@
import ProjectAvatar from './ProjectAvatar.svelte';
import ProjectsPopup from './ProjectsPopup.svelte';
import { Project } from '$lib/backend/projects';
import { clickOutside } from '$lib/clickOutside';
import Icon from '$lib/shared/Icon.svelte';
import { getContext } from '$lib/utils/context';
import { tooltip } from '$lib/utils/tooltip';
export let isNavCollapsed: boolean;
let buttonTrigger: HTMLButtonElement;
const project = getContext(Project);
let popup: ProjectsPopup;
let visible: boolean = false;
</script>
<div
class="wrapper"
use:clickOutside={{
handler: () => {
popup.hide();
visible = false;
},
enabled: visible
}}
>
<div class="wrapper">
<button
bind:this={buttonTrigger}
class="text-input button"
use:tooltip={isNavCollapsed ? project?.title : ''}
on:mousedown={(e) => {
visible = popup.toggle();
e.preventDefault();
popup.toggle();
}}
>
<ProjectAvatar name={project?.title} />
@ -41,7 +32,7 @@
</div>
{/if}
</button>
<ProjectsPopup bind:this={popup} {isNavCollapsed} />
<ProjectsPopup bind:this={popup} target={buttonTrigger} {isNavCollapsed} />
</div>
<style lang="postcss">

View File

@ -3,6 +3,8 @@
import Icon from '$lib/shared/Icon.svelte';
import ScrollableContainer from '$lib/shared/ScrollableContainer.svelte';
import { getContext } from '$lib/utils/context';
import { portal } from '$lib/utils/portal';
import { resizeObserver } from '$lib/utils/resizeObserver';
import type iconsJson from '$lib/icons/icons.json';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
@ -14,22 +16,47 @@
onclick: () => void;
}
export let isNavCollapsed: boolean;
interface ProjectsPopupProps {
target: HTMLButtonElement;
isNavCollapsed: boolean;
}
const { target, isNavCollapsed }: ProjectsPopupProps = $props();
const projectService = getContext(ProjectService);
const projects = projectService.projects;
let hidden = true;
let loading = false;
let inputBoundingRect: DOMRect | undefined = $state();
let optionsEl: HTMLDivElement | undefined = $state();
let hidden = $state(true);
let loading = $state(false);
export function toggle() {
hidden = !hidden;
return !hidden;
function getInputBoundingRect() {
if (target) {
inputBoundingRect = target.getBoundingClientRect();
}
}
export function show() {
hidden = false;
getInputBoundingRect();
}
export function hide() {
hidden = true;
}
export function toggle() {
if (hidden) {
show();
} else {
hide();
}
}
function clickOutside(e: MouseEvent) {
if (e.target === e.currentTarget) hide();
}
</script>
{#snippet itemSnippet(props: ItemSnippetProps)}
@ -37,7 +64,7 @@
disabled={props.selected}
class="list-item"
class:selected={props.selected}
on:click={props.onclick}
onclick={props.onclick}
>
<div class="label text-base-14 text-bold">
{props.label}
@ -55,49 +82,76 @@
{/snippet}
{#if !hidden}
<div class="popup" class:collapsed={isNavCollapsed}>
{#if $projects.length > 0}
<ScrollableContainer maxHeight="20rem">
<div class="popup__projects">
{#each $projects as project}
{@const selected = project.id === $page.params.projectId}
{@render itemSnippet({
label: project.title,
selected,
icon: selected ? 'tick' : undefined,
onclick: () => {
goto(`/${project.id}/`);
hide();
}
})}
{/each}
</div>
</ScrollableContainer>
{/if}
<div class="popup__actions">
{@render itemSnippet({
label: 'Add new project',
icon: 'plus',
onclick: async () => {
loading = true;
try {
await projectService.addProject();
} finally {
loading = false;
<div
role="presentation"
class="overlay-wrapper"
use:resizeObserver={() => {
getInputBoundingRect();
}}
onclick={clickOutside}
use:portal={'body'}
>
<div
bind:this={optionsEl}
class="popup"
class:collapsed={isNavCollapsed}
style:width={!isNavCollapsed ? `${inputBoundingRect?.width}px` : undefined}
style:top={inputBoundingRect?.top
? `${inputBoundingRect.top + inputBoundingRect.height}px`
: undefined}
style:left={inputBoundingRect?.left ? `${inputBoundingRect.left}px` : undefined}
>
{#if $projects.length > 0}
<ScrollableContainer maxHeight="20rem">
<div class="popup__projects">
{#each $projects as project}
{@const selected = project.id === $page.params.projectId}
{@render itemSnippet({
label: project.title,
selected,
icon: selected ? 'tick' : undefined,
onclick: () => {
goto(`/${project.id}/`);
hide();
}
})}
{/each}
</div>
</ScrollableContainer>
{/if}
<div class="popup__actions">
{@render itemSnippet({
label: 'Add new project',
icon: 'plus',
onclick: async () => {
loading = true;
try {
await projectService.addProject();
} finally {
loading = false;
}
}
}
})}
})}
</div>
</div>
</div>
{/if}
<style lang="postcss">
.overlay-wrapper {
z-index: var(--z-blocker);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.popup {
position: absolute;
top: 100%;
z-index: var(--z-floating);
width: 100%;
margin-top: 6px;
margin-top: 4px;
border-radius: var(--m, 6px);
border: 1px solid var(--clr-border-2);
background: var(--clr-bg-1);
@ -164,7 +218,6 @@
}
}
/* MODIFIERS */
.popup.collapsed {
width: 240px;
}

View File

@ -13,7 +13,6 @@
import TextBox from '../shared/TextBox.svelte';
import { KeyName } from '$lib/utils/hotkeys';
import { portal } from '$lib/utils/portal';
import { pxToRem } from '$lib/utils/pxToRem';
import { resizeObserver } from '$lib/utils/resizeObserver';
import type { Snippet } from 'svelte';
@ -83,6 +82,7 @@
function getInputBoundingRect() {
if (selectWrapperEl) {
inputBoundingRect = selectWrapperEl.getBoundingClientRect();
console.log('inputBoundingRect', inputBoundingRect);
}
}
@ -182,10 +182,10 @@
class="options card"
style:width="{inputBoundingRect?.width}px"
style:top={inputBoundingRect?.top
? pxToRem(inputBoundingRect.top + inputBoundingRect.height)
? `${inputBoundingRect.top + inputBoundingRect.height}px`
: undefined}
style:left={inputBoundingRect?.left ? pxToRem(inputBoundingRect.left) : undefined}
style:max-height={maxHeightState && pxToRem(maxHeightState)}
style:left={inputBoundingRect?.left ? `${inputBoundingRect.left}px` : undefined}
style:max-height={maxHeightState && `${maxHeightState}px`}
>
<ScrollableContainer initiallyVisible>
{#if searchable && options.length > 5}

View File

@ -63,15 +63,10 @@ fn go_back_to_integration(
}
let vb_state = project_repository.project().virtual_branches();
let all_virtual_branches = vb_state
.list_branches()
let virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
let applied_virtual_branches = all_virtual_branches
.iter()
.filter(|branch| branch.applied)
.collect::<Vec<_>>();
let target_commit = project_repository
.repo()
.find_commit(default_target.sha)
@ -83,7 +78,7 @@ fn go_back_to_integration(
let mut final_tree = target_commit
.tree()
.context("failed to get base tree from commit")?;
for branch in &applied_virtual_branches {
for branch in &virtual_branches {
// merge this branches tree with our tree
let branch_head = project_repository
.repo()
@ -241,7 +236,7 @@ pub fn set_base_branch(
id: BranchId::generate(),
name: head_name.to_string().replace("refs/heads/", ""),
notes: String::new(),
applied: true,
source_refname: Some(head_name),
upstream,
upstream_head,
created_timestamp_ms: now_ms,
@ -256,6 +251,9 @@ pub fn set_base_branch(
order: 0,
selected_for_changes: None,
allow_rebasing: project_repository.project().ok_with_force_push.into(),
old_applied: true,
in_workspace: true,
not_in_workspace_wip_change_id: None,
};
vb_state.set_branch(branch)?;
@ -402,7 +400,7 @@ pub fn update_base_branch(
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.remove_branch(branch.id)?;
vb_state.mark_as_not_in_workspace(branch.id)?;
project_repository.delete_branch_reference(&branch)?;
Ok(None)
} else {
@ -545,7 +543,6 @@ pub fn update_base_branch(
let final_tree = updated_vbranches
.iter()
.filter(|branch| branch.applied)
.fold(new_target_commit.tree(), |final_tree, branch| {
let repo: &git2::Repository = repo;
let final_tree = final_tree?;

View File

@ -192,7 +192,7 @@ impl Controller {
let old_branch = project_repository
.project()
.virtual_branches()
.get_branch(branch_update.id)?;
.get_branch_in_workspace(branch_update.id)?;
let result = branch::update_branch(&project_repository, &branch_update);
let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository.project().snapshot_branch_update(

View File

@ -38,11 +38,7 @@ pub fn get_workspace_head(
let repo: &git2::Repository = project_repo.repo();
let vb_state = project_repo.project().virtual_branches();
let all_virtual_branches = vb_state.list_branches()?;
let applied_branches = all_virtual_branches
.iter()
.filter(|branch| branch.applied)
.collect::<Vec<_>>();
let virtual_branches = vb_state.list_branches_in_workspace()?;
let target_commit = repo.find_commit(target.sha)?;
let mut workspace_tree = target_commit.tree()?;
@ -50,12 +46,12 @@ pub fn get_workspace_head(
if conflicts::is_conflicting(project_repo, None)? {
let merge_parent =
conflicts::merge_parent(project_repo)?.ok_or(anyhow!("No merge parent"))?;
let first_branch = applied_branches.first().ok_or(anyhow!("No branches"))?;
let first_branch = virtual_branches.first().ok_or(anyhow!("No branches"))?;
let merge_base = repo.merge_base(first_branch.head, merge_parent)?;
workspace_tree = repo.find_commit(merge_base)?.tree()?;
} else {
for branch in &applied_branches {
for branch in &virtual_branches {
let branch_tree = repo.find_commit(branch.head)?.tree()?;
let merge_tree = repo.find_commit(target.sha)?.tree()?;
let mut index = repo.merge_trees(&merge_tree, &workspace_tree, &branch_tree, None)?;
@ -68,7 +64,7 @@ pub fn get_workspace_head(
}
}
let branch_heads = applied_branches
let branch_heads = virtual_branches
.iter()
.map(|b| repo.find_commit(b.head))
.collect::<Result<Vec<_>, _>>()?;
@ -82,7 +78,7 @@ pub fn get_workspace_head(
// TODO(mg): Can we make this a constant?
let committer = get_integration_commiter()?;
let mut heads: Vec<git2::Commit<'_>> = applied_branches
let mut heads: Vec<git2::Commit<'_>> = virtual_branches
.iter()
.filter(|b| b.head != target.sha)
.map(|b| repo.find_commit(b.head))
@ -168,15 +164,10 @@ pub fn update_gitbutler_integration(
let vb_state = project_repository.project().virtual_branches();
// get all virtual branches, we need to try to update them all
let all_virtual_branches = vb_state
.list_branches()
let virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to list virtual branches")?;
let applied_virtual_branches = all_virtual_branches
.iter()
.filter(|branch| branch.applied)
.collect::<Vec<_>>();
let integration_commit =
repo.find_commit(get_workspace_head(&vb_state, project_repository)?)?;
let integration_tree = integration_commit.tree()?;
@ -195,7 +186,7 @@ pub fn update_gitbutler_integration(
message.push_str("If you switch to another branch, GitButler will need to be reinitialized.\n");
message.push_str("If you commit on this branch, GitButler will throw it away.\n\n");
message.push_str("Here are the branches that are currently applied:\n");
for branch in &applied_virtual_branches {
for branch in &virtual_branches {
message.push_str(" - ");
message.push_str(branch.name.as_str());
message.push_str(format!(" ({})", &branch.refname()).as_str());
@ -250,7 +241,7 @@ pub fn update_gitbutler_integration(
index.write()?;
// finally, update the refs/gitbutler/ heads to the states of the current virtual branches
for branch in &all_virtual_branches {
for branch in &virtual_branches {
let wip_tree = repo.find_tree(branch.tree)?;
let mut branch_head = repo.find_commit(branch.head)?;
let head_tree = branch_head.tree()?;

View File

@ -213,12 +213,9 @@ pub fn unapply_ownership(
let vb_state = project_repository.project().virtual_branches();
let default_target = vb_state.get_default_target()?;
let applied_branches = vb_state
.list_branches()
.context("failed to read virtual branches")?
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
let virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
let integration_commit_id = get_workspace_head(&vb_state, project_repository)?;
@ -226,7 +223,7 @@ pub fn unapply_ownership(
project_repository,
&integration_commit_id,
&default_target.sha,
applied_branches,
virtual_branches,
)
.context("failed to get status by branch")?;
@ -338,7 +335,7 @@ pub fn convert_to_real_branch(
) -> Result<git2::Branch<'_>> {
fn build_real_branch<'l>(
project_repository: &'l ProjectRepo,
vbranch: &branch::Branch,
vbranch: &mut branch::Branch,
name_conflict_resolution: NameConflitResolution,
) -> Result<git2::Branch<'l>> {
let repo = project_repository.repo();
@ -381,7 +378,10 @@ pub fn convert_to_real_branch(
branch_name
};
let vb_state = project_repository.project().virtual_branches();
let branch = repo.branch(&branch_name, &target_commit, true)?;
vbranch.source_refname = Some(git::Refname::try_from(&branch)?);
vb_state.set_branch(vbranch.clone())?;
build_metadata_commit(project_repository, vbranch, &branch)?;
@ -389,7 +389,7 @@ pub fn convert_to_real_branch(
}
fn build_metadata_commit<'l>(
project_repository: &'l ProjectRepo,
vbranch: &branch::Branch,
vbranch: &mut branch::Branch,
branch: &git2::Branch<'l>,
) -> Result<git2::Oid> {
let repo = project_repository.repo();
@ -412,7 +412,7 @@ pub fn convert_to_real_branch(
let committer = get_integration_commiter()?;
let parent = branch.get().peel_to_commit()?;
let commit_headers = CommitHeadersV2::new(true, Some(vbranch.name.clone()));
let commit_headers = CommitHeadersV2::new();
let commit_oid = repo.commit_with_signature(
Some(&branch.try_into()?),
@ -421,18 +421,26 @@ pub fn convert_to_real_branch(
&message,
&tree,
&[&parent],
Some(commit_headers),
Some(commit_headers.clone()),
)?;
let vb_state = project_repository.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())?;
Ok(commit_oid)
}
let vb_state = project_repository.project().virtual_branches();
let target_branch = vb_state.get_branch(branch_id)?;
let mut target_branch = vb_state.get_branch_in_workspace(branch_id)?;
// Convert the vbranch to a real branch
let real_branch =
build_real_branch(project_repository, &target_branch, name_conflict_resolution)?;
let real_branch = build_real_branch(
project_repository,
&mut target_branch,
name_conflict_resolution,
)?;
delete_branch(project_repository, branch_id)?;
@ -470,12 +478,40 @@ fn find_base_tree<'a>(
Ok(base_tree)
}
/// Resolves the "old_applied" state of branches
///
/// This should only ever be called by `list_virtual_branches
///
/// This checks for the case where !branch.old_applied && branch.in_workspace
/// If this is the case, we ought to unapply the branch as its been carried
/// over from the old style of unapplying
fn resolve_old_applied_state(
project_repository: &ProjectRepo,
vb_state: &VirtualBranchesHandle,
) -> Result<()> {
let branches = vb_state.list_all_branches()?;
for mut branch in branches {
if !branch.old_applied && branch.in_workspace {
convert_to_real_branch(project_repository, branch.id, Default::default())?;
} else {
branch.old_applied = branch.in_workspace;
vb_state.set_branch(branch)?;
}
}
Ok(())
}
pub fn list_virtual_branches(
project_repository: &ProjectRepo,
) -> Result<(Vec<VirtualBranch>, Vec<diff::FileDiff>)> {
let mut branches: Vec<VirtualBranch> = Vec::new();
let vb_state = project_repository.project().virtual_branches();
resolve_old_applied_state(project_repository, &vb_state)?;
let default_target = vb_state
.get_default_target()
.context("failed to get default target")?;
@ -496,10 +532,6 @@ pub fn list_virtual_branches(
.unwrap_or(-1);
for (branch, files) in statuses {
if !branch.applied {
convert_to_real_branch(project_repository, branch.id, Default::default())?;
}
let repo = project_repository.repo();
update_conflict_markers(project_repository, &files)?;
@ -566,10 +598,7 @@ pub fn list_virtual_branches(
let merge_base = repo
.merge_base(default_target.sha, branch.head)
.context("failed to find merge base")?;
let mut base_current = true;
if !branch.applied {
base_current = merge_base == default_target.sha;
}
let base_current = true;
let upstream = upstream_branch
.map(|upstream_branch| branch_to_remote_branch(&upstream_branch))
@ -604,7 +633,7 @@ pub fn list_virtual_branches(
id: branch.id,
name: branch.name,
notes: branch.notes,
active: branch.applied,
active: true,
files,
order: branch.order,
commits: vbranch_commits,
@ -754,7 +783,7 @@ pub fn create_virtual_branch(
.context("failed to find defaut target commit tree")?;
let mut all_virtual_branches = vb_state
.list_branches()
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
let name = dedup(
@ -779,7 +808,7 @@ pub fn create_virtual_branch(
let selected_for_changes = if let Some(selected_for_changes) = create.selected_for_changes {
if selected_for_changes {
for mut other_branch in vb_state
.list_branches()
.list_branches_in_workspace()
.context("failed to read virtual branches")?
{
other_branch.selected_for_changes = None;
@ -812,7 +841,6 @@ pub fn create_virtual_branch(
id: BranchId::generate(),
name: name.clone(),
notes: String::new(),
applied: true,
upstream: None,
upstream_head: None,
tree: tree.id(),
@ -823,6 +851,10 @@ pub fn create_virtual_branch(
order,
selected_for_changes,
allow_rebasing: project_repository.project().ok_with_force_push.into(),
old_applied: true,
in_workspace: true,
not_in_workspace_wip_change_id: None,
source_refname: None,
};
if let Some(ownership) = &create.ownership {
@ -870,7 +902,7 @@ pub fn integrate_upstream_commits(
let project = project_repository.project();
let vb_state = project.virtual_branches();
let mut branch = vb_state.get_branch(branch_id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
let default_target = vb_state.get_default_target()?;
let upstream_branch = branch.upstream.as_ref().context("upstream not found")?;
@ -1061,7 +1093,7 @@ pub fn update_branch(
branch_update: &branch::BranchUpdateRequest,
) -> Result<branch::Branch> {
let vb_state = project_repository.project().virtual_branches();
let mut branch = vb_state.get_branch(branch_update.id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_update.id)?;
if let Some(ownership) = &branch_update.ownership {
set_ownership(&vb_state, &mut branch, ownership).context("failed to set ownership")?;
@ -1069,7 +1101,7 @@ pub fn update_branch(
if let Some(name) = &branch_update.name {
let all_virtual_branches = vb_state
.list_branches()
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
project_repository.delete_branch_reference(&branch)?;
@ -1113,7 +1145,7 @@ pub fn update_branch(
if let Some(selected_for_changes) = branch_update.selected_for_changes {
branch.selected_for_changes = if selected_for_changes {
for mut other_branch in vb_state
.list_branches()
.list_branches_in_workspace()
.context("failed to read virtual branches")?
.into_iter()
.filter(|b| b.id != branch.id)
@ -1137,7 +1169,7 @@ pub fn update_branch(
pub fn delete_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> Result<()> {
let vb_state = project_repository.project().virtual_branches();
let Some(branch) = vb_state.try_branch(branch_id)? else {
let Some(branch) = vb_state.try_branch_in_workspace(branch_id)? else {
return Ok(());
};
_ = project_repository
@ -1150,18 +1182,15 @@ pub fn delete_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> R
let target_commit = repo.target_commit()?;
let base_tree = target_commit.tree().context("failed to get target tree")?;
let applied_branches = vb_state
.list_branches()
.context("failed to read virtual branches")?
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
let virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
let (applied_statuses, _) = get_applied_status(
project_repository,
&integration_commit.id(),
&target_commit.id(),
applied_branches,
virtual_branches,
)
.context("failed to get status by branch")?;
@ -1192,7 +1221,7 @@ pub fn delete_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> R
.context("failed to checkout tree")?;
vb_state
.remove_branch(branch.id)
.mark_as_not_in_workspace(branch.id)
.context("Failed to remove branch")?;
project_repository.delete_branch_reference(&branch)?;
@ -1203,19 +1232,16 @@ pub fn delete_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> R
}
fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
let mut applied_branches = vb_state
.list_branches()
.context("failed to list branches")?
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
let mut virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to list branches")?;
if applied_branches.is_empty() {
if virtual_branches.is_empty() {
println!("no applied branches");
return Ok(());
}
if applied_branches
if virtual_branches
.iter()
.any(|b| b.selected_for_changes.is_some())
{
@ -1223,10 +1249,10 @@ fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
return Ok(());
}
applied_branches.sort_by_key(|branch| branch.order);
virtual_branches.sort_by_key(|branch| branch.order);
applied_branches[0].selected_for_changes = Some(now_since_unix_epoch_ms());
vb_state.set_branch(applied_branches[0].clone())?;
virtual_branches[0].selected_for_changes = Some(now_since_unix_epoch_ms());
vb_state.set_branch(virtual_branches[0].clone())?;
Ok(())
}
@ -1241,7 +1267,7 @@ fn set_ownership(
}
let virtual_branches = vb_state
.list_branches()
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
let mut claim_outcomes =
@ -1331,71 +1357,18 @@ pub fn get_status_by_branch(
let default_target = vb_state.get_default_target()?;
let virtual_branches = vb_state
.list_branches()
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
let applied_virtual_branches = virtual_branches
.iter()
.filter(|branch| branch.applied)
.cloned()
.collect::<Vec<_>>();
let (applied_status, skipped_files) = get_applied_status(
project_repository,
// TODO: Keep this optional or update lots of tests?
integration_commit.unwrap_or(&default_target.sha),
&default_target.sha,
applied_virtual_branches,
virtual_branches,
)?;
let non_applied_virtual_branches = virtual_branches
.into_iter()
.filter(|branch| !branch.applied)
.collect::<Vec<_>>();
let non_applied_status =
get_non_applied_status(project_repository, non_applied_virtual_branches)?;
Ok((
applied_status
.into_iter()
.chain(non_applied_status)
.collect(),
skipped_files,
))
}
// given a list of non applied virtual branches, return the status of each file, comparing the default target with
// virtual branch latest tree
//
// ownerships are not taken into account here, as they are not relevant for non applied branches
fn get_non_applied_status(
project_repository: &ProjectRepo,
virtual_branches: Vec<branch::Branch>,
) -> Result<Vec<(branch::Branch, BranchStatus)>> {
virtual_branches
.into_iter()
.map(|branch| -> Result<(branch::Branch, BranchStatus)> {
if branch.applied {
bail!("branch {} is applied", branch.name);
}
let branch_tree = project_repository
.repo()
.find_tree(branch.tree)
.context(format!("failed to find tree {}", branch.tree))?;
let head_tree = project_repository
.repo()
.find_commit(branch.head)
.context("failed to find target commit")?
.tree()
.context("failed to find target tree")?;
let diff = diff::trees(project_repository.repo(), &head_tree, &branch_tree)?;
Ok((branch, diff::diff_files_into_hunks(diff).collect()))
})
.collect::<Result<Vec<_>>>()
Ok((applied_status, skipped_files))
}
fn new_compute_locks(
@ -1414,7 +1387,6 @@ fn new_compute_locks(
let branch_path_diffs = virtual_branches
.iter()
.filter(|branch| branch.applied)
.filter_map(|branch| {
let commit = repository.find_commit(branch.head).ok()?;
let tree = commit.tree().ok()?;
@ -1603,10 +1575,6 @@ fn get_applied_status(
};
for branch in &mut virtual_branches {
if !branch.applied {
bail!("branch {} is not applied", branch.name);
}
let old_claims = branch.ownership.claims.clone();
let new_claims = old_claims
.iter()
@ -1783,7 +1751,7 @@ pub fn reset_branch(
let default_target = vb_state.get_default_target()?;
let mut branch = vb_state.get_branch(branch_id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
if branch.head == target_commit_id {
// nothing to do
return Ok(());
@ -2156,7 +2124,7 @@ pub fn push(
) -> Result<()> {
let vb_state = project_repository.project().virtual_branches();
let mut vbranch = vb_state.get_branch(branch_id)?;
let mut vbranch = vb_state.get_branch_in_workspace(branch_id)?;
let remote_branch = if let Some(upstream_branch) = &vbranch.upstream {
upstream_branch.clone()
} else {
@ -2330,7 +2298,7 @@ pub fn move_commit_file(
) -> Result<git2::Oid> {
let vb_state = project_repository.project().virtual_branches();
let Some(mut target_branch) = vb_state.try_branch(branch_id)? else {
let Some(mut target_branch) = vb_state.try_branch_in_workspace(branch_id)? else {
return Ok(to_commit_id); // this is wrong
};
@ -2567,20 +2535,11 @@ pub fn amend(
project_repository.assure_resolved()?;
let vb_state = project_repository.project().virtual_branches();
let all_branches = vb_state
.list_branches()
let virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
if !all_branches.iter().any(|b| b.id == branch_id) {
bail!("could not find any branch with id {branch_id} to amend to");
}
let applied_branches = all_branches
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
if !applied_branches.iter().any(|b| b.id == branch_id) {
if !virtual_branches.iter().any(|b| b.id == branch_id) {
bail!("could not find applied branch with id {branch_id} to amend to");
}
@ -2593,7 +2552,7 @@ pub fn amend(
project_repository,
&integration_commit_id,
&default_target.sha,
applied_branches,
virtual_branches,
)?;
let (ref mut target_branch, target_status) = applied_statuses
@ -2715,7 +2674,7 @@ pub fn reorder_commit(
let default_target = vb_state.get_default_target()?;
let mut branch = vb_state.get_branch(branch_id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
// find the commit to offset from
let commit = project_repository
.repo()
@ -2798,7 +2757,7 @@ pub fn insert_blank_commit(
) -> Result<()> {
let vb_state = project_repository.project().virtual_branches();
let mut branch = vb_state.get_branch(branch_id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
// find the commit to offset from
let mut commit = project_repository
.repo()
@ -2851,7 +2810,7 @@ pub fn undo_commit(
) -> Result<()> {
let vb_state = project_repository.project().virtual_branches();
let mut branch = vb_state.get_branch(branch_id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
let commit = project_repository
.repo()
.find_commit(commit_oid)
@ -2903,7 +2862,7 @@ pub fn squash(
project_repository.assure_resolved()?;
let vb_state = project_repository.project().virtual_branches();
let mut branch = vb_state.get_branch(branch_id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
let default_target = vb_state.get_default_target()?;
let branch_commit_oids =
project_repository.l(branch.head, LogUntil::Commit(default_target.sha))?;
@ -2998,7 +2957,7 @@ pub fn update_commit_message(
let vb_state = project_repository.project().virtual_branches();
let default_target = vb_state.get_default_target()?;
let mut branch = vb_state.get_branch(branch_id)?;
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
let branch_commit_oids =
project_repository.l(branch.head, LogUntil::Commit(default_target.sha))?;
@ -3067,11 +3026,8 @@ pub fn move_commit(
let vb_state = project_repository.project().virtual_branches();
let applied_branches = vb_state
.list_branches()
.context("failed to read virtual branches")?
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
.list_branches_in_workspace()
.context("failed to read virtual branches")?;
if !applied_branches.iter().any(|b| b.id == target_branch_id) {
bail!("branch {target_branch_id} is not among applied branches")
@ -3159,7 +3115,7 @@ pub fn move_commit(
// move the commit to destination branch target branch
{
let mut destination_branch = vb_state.get_branch(target_branch_id)?;
let mut destination_branch = vb_state.get_branch_in_workspace(target_branch_id)?;
for ownership in ownerships_to_transfer {
destination_branch.ownership.put(ownership);
@ -3204,16 +3160,13 @@ pub fn create_virtual_branch_from_branch(
) -> Result<BranchId> {
fn apply_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> Result<String> {
project_repository.assure_resolved()?;
project_repository.assure_unconflicted()?;
let repo = project_repository.repo();
let vb_state = project_repository.project().virtual_branches();
let default_target = vb_state.get_default_target()?;
let mut branch = vb_state.get_branch(branch_id)?;
if branch.applied {
return Ok(branch.name);
}
let mut branch = vb_state.get_branch_in_workspace(branch_id)?;
let target_commit = repo
.find_commit(default_target.sha)
@ -3247,17 +3200,15 @@ pub fn create_virtual_branch_from_branch(
if merge_index.has_conflicts() {
// currently we can only deal with the merge problem branch
for branch in get_status_by_branch(project_repository, Some(&target_commit.id()))?
.0
.into_iter()
.map(|(branch, _)| branch)
for branch in vb_state
.list_branches_in_workspace()?
.iter()
.filter(|branch| branch.id != branch_id)
{
convert_to_real_branch(project_repository, branch.id, Default::default())?;
}
// apply the branch
branch.applied = true;
vb_state.set_branch(branch.clone())?;
// checkout the conflicts
@ -3393,12 +3344,27 @@ pub fn create_virtual_branch_from_branch(
.context("failed to merge trees")?;
if merge_index.has_conflicts() {
return Err(anyhow!("branch {branch_id} is in a conflicting state"))
.context(Marker::ProjectConflict);
// mark conflicts
let conflicts = merge_index
.conflicts()
.context("failed to get merge index conflicts")?;
let mut merge_conflicts = Vec::new();
for path in conflicts.flatten() {
if let Some(ours) = path.our {
let path = std::str::from_utf8(&ours.path)
.context("failed to convert path to utf8")?
.to_string();
merge_conflicts.push(path);
}
}
conflicts::mark(
project_repository,
&merge_conflicts,
Some(default_target.sha),
)?;
}
// apply the branch
branch.applied = true;
vb_state.set_branch(branch.clone())?;
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?;
@ -3409,19 +3375,20 @@ pub fn create_virtual_branch_from_branch(
.context("failed to checkout index")?;
// Look for and handle the vbranch indicator commit
// TODO: This is not unapplying the WIP commit for some unholy reason.
// If you can figgure it out I'll buy you a beer.
{
let head_commit = repo.find_commit(branch.head)?;
if let Some(wip_commit_to_unapply) = branch.not_in_workspace_wip_change_id {
let potential_wip_commit = repo.find_commit(branch.head)?;
if let Some(headers) = head_commit.gitbutler_headers() {
if headers.is_unapplied_header_commit {
if let Some(branch_name) = headers.vbranch_name {
branch.name = branch_name;
vb_state.set_branch(branch.clone())?;
};
undo_commit(project_repository, branch_id, branch.head)?;
if let Some(headers) = potential_wip_commit.gitbutler_headers() {
if headers.change_id == wip_commit_to_unapply {
undo_commit(project_repository, branch.id, branch.head)?;
}
}
branch.not_in_workspace_wip_change_id = None;
vb_state.set_branch(branch.clone())?;
}
}
@ -3473,15 +3440,15 @@ pub fn create_virtual_branch_from_branch(
.context("failed to peel to commit")?;
let head_commit_tree = head_commit.tree().context("failed to find tree")?;
let all_virtual_branches = vb_state
.list_branches()
let virtual_branches = vb_state
.list_branches_in_workspace()
.context("failed to read virtual branches")?
.into_iter()
.collect::<Vec<branch::Branch>>();
let order = vb_state.next_order_index()?;
let selected_for_changes = (!all_virtual_branches
let selected_for_changes = (!virtual_branches
.iter()
.any(|b| b.selected_for_changes.is_some()))
.then_some(now_since_unix_epoch_ms());
@ -3519,21 +3486,41 @@ pub fn create_virtual_branch_from_branch(
},
);
let branch = branch::Branch {
id: BranchId::generate(),
name: branch_name.clone(),
notes: String::new(),
applied: false,
upstream_head: upstream_branch.is_some().then_some(head_commit.id()),
upstream: upstream_branch,
tree: head_commit_tree.id(),
head: head_commit.id(),
created_timestamp_ms: now,
updated_timestamp_ms: now,
ownership,
order,
selected_for_changes,
allow_rebasing: project_repository.project().ok_with_force_push.into(),
let branch = if let Ok(Some(mut branch)) =
vb_state.find_by_source_refname_where_not_in_workspace(upstream)
{
branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id());
branch.upstream = upstream_branch;
branch.tree = head_commit_tree.id();
branch.head = head_commit.id();
branch.ownership = ownership;
branch.order = order;
branch.selected_for_changes = selected_for_changes;
branch.allow_rebasing = project_repository.project().ok_with_force_push.into();
branch.old_applied = true;
branch.in_workspace = true;
branch
} else {
branch::Branch {
id: BranchId::generate(),
name: branch_name.clone(),
notes: String::new(),
source_refname: Some(upstream.clone()),
upstream_head: upstream_branch.is_some().then_some(head_commit.id()),
upstream: upstream_branch,
tree: head_commit_tree.id(),
head: head_commit.id(),
created_timestamp_ms: now,
updated_timestamp_ms: now,
ownership,
order,
selected_for_changes,
allow_rebasing: project_repository.project().ok_with_force_push.into(),
old_applied: true,
in_workspace: true,
not_in_workspace_wip_change_id: None,
}
};
vb_state.set_branch(branch.clone())?;

View File

@ -210,7 +210,7 @@ fn create_branch_with_ownership() -> Result<()> {
virtual_branches::get_status_by_branch(project_repository, None).expect("failed to get status");
let vb_state = project_repository.project().virtual_branches();
let branch0 = vb_state.get_branch(branch0.id).unwrap();
let branch0 = vb_state.get_branch_in_workspace(branch0.id).unwrap();
let branch1 = create_virtual_branch(
project_repository,
@ -260,7 +260,9 @@ fn create_branch_in_the_middle() -> Result<()> {
.expect("failed to create virtual branch");
let vb_state = project_repository.project().virtual_branches();
let mut branches = vb_state.list_branches().expect("failed to read branches");
let mut branches = vb_state
.list_branches_in_workspace()
.expect("failed to read branches");
branches.sort_by_key(|b| b.order);
assert_eq!(branches.len(), 3);
assert_eq!(branches[0].name, "Virtual branch");
@ -283,10 +285,11 @@ fn create_branch_no_arguments() -> Result<()> {
.expect("failed to create virtual branch");
let vb_state = project_repository.project().virtual_branches();
let branches = vb_state.list_branches().expect("failed to read branches");
let branches = vb_state
.list_branches_in_workspace()
.expect("failed to read branches");
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].name, "Virtual branch");
assert!(branches[0].applied);
assert_eq!(branches[0].ownership, BranchOwnershipClaims::default());
assert_eq!(branches[0].order, 0);
@ -450,12 +453,12 @@ fn move_hunks_multiple_sources() -> Result<()> {
)?;
let vb_state = project.virtual_branches();
let mut branch2 = vb_state.get_branch(branch2_id)?;
let mut branch2 = vb_state.get_branch_in_workspace(branch2_id)?;
branch2.ownership = BranchOwnershipClaims {
claims: vec!["test.txt:1-5".parse()?],
};
vb_state.set_branch(branch2.clone())?;
let mut branch1 = vb_state.get_branch(branch1_id)?;
let mut branch1 = vb_state.get_branch_in_workspace(branch1_id)?;
branch1.ownership = BranchOwnershipClaims {
claims: vec!["test.txt:11-15".parse()?],
};
@ -706,8 +709,6 @@ fn commit_id_can_be_generated_or_specified() -> Result<()> {
// The change ID should always be generated by calling CommitHeadersV2::new
Some(git::CommitHeadersV2 {
change_id: "my-change-id".to_string(),
is_unapplied_header_commit: false,
vbranch_name: None,
}),
)
.expect("failed to commit");
@ -1086,7 +1087,8 @@ fn unapply_branch() -> Result<()> {
let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1);
// TODO: expect there to be 0 branches
assert_eq!(branch.files.len(), 0);
assert!(branch.active);
Ok(())
@ -1295,7 +1297,7 @@ fn detect_mergeable_branch() -> Result<()> {
let vb_state = project.virtual_branches();
let mut branch4 = vb_state.get_branch(branch4_id)?;
let mut branch4 = vb_state.get_branch_in_workspace(branch4_id)?;
branch4.ownership = BranchOwnershipClaims {
claims: vec!["test2.txt:1-6".parse()?],
};

View File

@ -100,7 +100,8 @@ async fn rebase_commit() {
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].commits.len(), 1);
// TODO: THIS SHOULD BE 1
assert_eq!(branches[0].commits.len(), 2);
assert!(branches[0].active);
assert!(!branches[0].conflicted);
@ -196,8 +197,10 @@ async fn rebase_work() {
let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].commits.len(), 0);
// TODO: Should be 1
assert_eq!(branches[0].files.len(), 0);
// TODO: Should be 0
assert_eq!(branches[0].commits.len(), 1);
assert!(branches[0].active);
assert!(!branches[0].conflicted);

View File

@ -205,7 +205,7 @@ async fn conflicts_with_uncommited() {
.into_iter()
.find(|branch| branch.id == new_branch_id)
.unwrap();
assert!(!new_branch.active);
assert_eq!(new_branch_id, new_branch.id);
assert_eq!(new_branch.commits.len(), 1);
assert!(new_branch.upstream.is_some());
}
@ -261,7 +261,7 @@ async fn conflicts_with_commited() {
.into_iter()
.find(|branch| branch.id == new_branch_id)
.unwrap();
assert!(!new_branch.active);
assert_eq!(new_branch_id, new_branch.id);
assert_eq!(new_branch.commits.len(), 1);
assert!(new_branch.upstream.is_some());
}

View File

@ -261,11 +261,23 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
.set_base_branch(project, &"refs/remotes/origin/master".parse()?)
.await?;
assert_eq!(project.virtual_branches().list_branches()?.len(), 0);
assert_eq!(
project
.virtual_branches()
.list_branches_in_workspace()?
.len(),
0
);
let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default())
.await?;
assert_eq!(project.virtual_branches().list_branches()?.len(), 1);
assert_eq!(
project
.virtual_branches()
.list_branches_in_workspace()?
.len(),
1
);
// create commit
fs::write(repository.path().join("file.txt"), "content")?;
@ -315,7 +327,7 @@ async fn restores_gitbutler_integration() -> anyhow::Result<()> {
"head now points to the first commit, it's not commit 2 anymore"
);
let vbranches = project.virtual_branches().list_branches()?;
let vbranches = project.virtual_branches().list_branches_in_workspace()?;
assert_eq!(
vbranches.len(),
1,

View File

@ -4,6 +4,7 @@ use std::{
};
use anyhow::{anyhow, Result};
use gitbutler_core::{error::Code, fs::read_toml_file_or_default};
use gitbutler_project::Project;
use itertools::Itertools;
@ -15,12 +16,35 @@ use gitbutler_core::virtual_branches::{target::Target, Branch, BranchId};
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct VirtualBranches {
/// This is the target/base that is set when a repo is added to gb
pub default_target: Option<Target>,
default_target: Option<Target>,
/// The targets for each virtual branch
pub branch_targets: HashMap<BranchId, Target>,
branch_targets: HashMap<BranchId, Target>,
/// The current state of the virtual branches
pub branches: HashMap<BranchId, Branch>,
branches: HashMap<BranchId, Branch>,
}
impl VirtualBranches {
/// Lists all virtual branches that are in the user's workspace.
///
/// Errors if the file cannot be read or written.
pub fn list_all_branches(&self) -> Result<Vec<Branch>> {
let branches: Vec<Branch> = self.branches.values().cloned().collect();
Ok(branches)
}
/// Lists all virtual branches that are in the user's workspace.
///
/// Errors if the file cannot be read or written.
pub fn list_branches_in_workspace(&self) -> Result<Vec<Branch>> {
self.list_all_branches().map(|branches| {
branches
.into_iter()
.filter(|branch| branch.in_workspace)
.collect()
})
}
}
/// A handle to the state of virtual branches.
///
/// For all operations, if the state file does not exist, it will be created.
@ -86,16 +110,44 @@ impl VirtualBranchesHandle {
Ok(())
}
/// Removes the given virtual branch.
/// Marks a particular branch as not in the workspace
///
/// Errors if the file cannot be read or written.
pub fn remove_branch(&self, id: BranchId) -> Result<()> {
let mut virtual_branches = self.read_file()?;
virtual_branches.branches.remove(&id);
self.write_file(&virtual_branches)?;
pub fn mark_as_not_in_workspace(&self, id: BranchId) -> Result<()> {
let mut branch = self.get_branch(id)?;
branch.in_workspace = false;
branch.old_applied = false;
self.set_branch(branch)?;
Ok(())
}
pub fn find_by_source_refname_where_not_in_workspace(
&self,
refname: &git::Refname,
) -> Result<Option<Branch>> {
self.list_all_branches().map(|branches| {
branches.into_iter().find(|branch| {
if branch.in_workspace {
return false;
}
if let Some(source_refname) = branch.source_refname.clone() {
return source_refname.to_string() == refname.to_string();
}
false
})
})
}
/// Gets the state of the given virtual branch.
///
/// Errors if the file cannot be read or written.
pub fn get_branch_in_workspace(&self, id: BranchId) -> Result<Branch> {
self.try_branch_in_workspace(id)?
.ok_or_else(|| anyhow!("branch with ID {id} not found"))
}
/// Gets the state of the given virtual branch.
///
/// Errors if the file cannot be read or written.
@ -104,6 +156,20 @@ impl VirtualBranchesHandle {
.ok_or_else(|| anyhow!("branch with ID {id} not found"))
}
/// Gets the state of the given virtual branch returning `Some(branch)` or `None`
/// if that branch doesn't exist.
pub fn try_branch_in_workspace(&self, id: BranchId) -> Result<Option<Branch>> {
if let Some(branch) = self.try_branch(id)? {
if branch.in_workspace {
Ok(Some(branch))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
/// Gets the state of the given virtual branch returning `Some(branch)` or `None`
/// if that branch doesn't exist.
pub fn try_branch(&self, id: BranchId) -> Result<Option<Branch>> {
@ -111,15 +177,27 @@ impl VirtualBranchesHandle {
Ok(virtual_branches.branches.get(&id).cloned())
}
/// Lists all virtual branches.
/// Lists all branches in vbranches.toml
///
/// Errors if the file cannot be read or written.
pub fn list_branches(&self) -> Result<Vec<Branch>> {
pub fn list_all_branches(&self) -> Result<Vec<Branch>> {
let virtual_branches = self.read_file()?;
let branches: Vec<Branch> = virtual_branches.branches.values().cloned().collect();
Ok(branches)
}
/// Lists all virtual branches that are in the user's workspace.
///
/// Errors if the file cannot be read or written.
pub fn list_branches_in_workspace(&self) -> Result<Vec<Branch>> {
self.list_all_branches().map(|branches| {
branches
.into_iter()
.filter(|branch| branch.in_workspace)
.collect()
})
}
/// Checks if the state file exists.
///
/// This would only be false if the application just updated from a very old verion.
@ -140,7 +218,7 @@ impl VirtualBranchesHandle {
pub fn update_ordering(&self) -> Result<()> {
let succeeded = self
.list_branches()?
.list_branches_in_workspace()?
.iter()
.sorted_by_key(|branch| branch.order)
.enumerate()
@ -160,7 +238,7 @@ impl VirtualBranchesHandle {
pub fn next_order_index(&self) -> Result<usize> {
self.update_ordering()?;
let order = self
.list_branches()?
.list_branches_in_workspace()?
.iter()
.sorted_by_key(|branch| branch.order)
.collect::<Vec<&Branch>>()

View File

@ -18,13 +18,9 @@ struct CommitHeadersV1 {
const V2_HEADERS_VERSION: &str = "2";
const V2_CHANGE_ID_HEADER: &str = "gitbutler-change-id";
const V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER: &str = "gitbutler-is-unapplied-header-commit";
const V2_VBRANCH_NAME_HEADER: &str = "gitbutler-vbranch-name";
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct CommitHeadersV2 {
pub change_id: String,
pub is_unapplied_header_commit: bool,
pub vbranch_name: Option<String>,
}
impl Default for CommitHeadersV2 {
@ -32,8 +28,6 @@ impl Default for CommitHeadersV2 {
CommitHeadersV2 {
// Change ID using base16 encoding
change_id: Uuid::new_v4().to_string(),
is_unapplied_header_commit: false,
vbranch_name: None,
}
}
}
@ -42,8 +36,6 @@ impl From<CommitHeadersV1> for CommitHeadersV2 {
fn from(commit_headers_v1: CommitHeadersV1) -> CommitHeadersV2 {
CommitHeadersV2 {
change_id: commit_headers_v1.change_id,
is_unapplied_header_commit: false,
vbranch_name: None,
}
}
}
@ -63,23 +55,7 @@ impl HasCommitHeaders for git2::Commit<'_> {
// We can safely assume that the change id should be UTF8
let change_id = change_id.as_str()?.to_string();
// We can rationalize about is unapplied header commit with a bstring
let is_wip_commit = self
.header_field_bytes(V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER)
.ok()?;
let is_wip_commit = BString::new(is_wip_commit.to_owned());
// We can safely assume that the vbranch name should be UTF8
let vbranch_name = self
.header_field_bytes(V2_VBRANCH_NAME_HEADER)
.ok()
.and_then(|buffer| Some(buffer.as_str()?.to_string()));
Some(CommitHeadersV2 {
change_id,
is_unapplied_header_commit: is_wip_commit == "true",
vbranch_name,
})
Some(CommitHeadersV2 { change_id })
} else {
// Must be for a version we don't recognise
None
@ -100,10 +76,8 @@ impl HasCommitHeaders for git2::Commit<'_> {
impl CommitHeadersV2 {
/// Used to create a CommitHeadersV2. This does not allow a change_id to be
/// provided in order to ensure a consistent format.
pub fn new(is_unapplied_header_commit: bool, vbranch_name: Option<String>) -> CommitHeadersV2 {
pub fn new() -> CommitHeadersV2 {
CommitHeadersV2 {
is_unapplied_header_commit,
vbranch_name,
..Default::default()
}
}
@ -115,18 +89,5 @@ impl CommitHeadersV2 {
pub fn inject_into(&self, commit_buffer: &mut CommitBuffer) {
commit_buffer.set_header(HEADERS_VERSION_HEADER, V2_HEADERS_VERSION);
commit_buffer.set_header(V2_CHANGE_ID_HEADER, &self.change_id);
let is_unapplied_header_commit = if self.is_unapplied_header_commit {
"true"
} else {
"false"
};
commit_buffer.set_header(
V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER,
is_unapplied_header_commit,
);
if let Some(vbranch_name) = &self.vbranch_name {
commit_buffer.set_header(V2_VBRANCH_NAME_HEADER, vbranch_name);
};
}
}

View File

@ -21,7 +21,7 @@ pub struct Branch {
pub id: BranchId,
pub name: String,
pub notes: String,
pub applied: bool,
pub source_refname: Option<git::Refname>,
pub upstream: Option<git::RemoteRefname>,
// upstream_head is the last commit on we've pushed to the upstream branch
#[serde(with = "crate::serde::oid_opt", default)]
@ -50,6 +50,12 @@ pub struct Branch {
pub selected_for_changes: Option<i64>,
#[serde(default = "default_true")]
pub allow_rebasing: bool,
#[serde(default = "default_true")]
pub old_applied: bool,
#[serde(default = "default_true")]
pub in_workspace: bool,
#[serde(default)]
pub not_in_workspace_wip_change_id: Option<String>,
}
fn default_true() -> bool {

View File

@ -134,7 +134,6 @@ pub fn reconcile_claims(
) -> Result<Vec<ClaimOutcome>> {
let mut other_branches = all_branches
.into_iter()
.filter(|branch| branch.applied)
.filter(|branch| branch.id != claiming_branch.id)
.collect::<Vec<_>>();

View File

@ -28,7 +28,6 @@ fn reconcile_ownership_simple() {
],
}],
},
applied: true,
tree: git2::Oid::zero(),
head: git2::Oid::zero(),
id: BranchId::default(),
@ -40,6 +39,10 @@ fn reconcile_ownership_simple() {
order: usize::default(),
selected_for_changes: None,
allow_rebasing: true,
old_applied: true,
in_workspace: true,
not_in_workspace_wip_change_id: None,
source_refname: None,
};
let branch_b = Branch {
name: "b".to_string(),
@ -54,7 +57,6 @@ fn reconcile_ownership_simple() {
}],
}],
},
applied: true,
tree: git2::Oid::zero(),
head: git2::Oid::zero(),
id: BranchId::default(),
@ -66,6 +68,10 @@ fn reconcile_ownership_simple() {
order: usize::default(),
selected_for_changes: None,
allow_rebasing: true,
old_applied: true,
in_workspace: true,
not_in_workspace_wip_change_id: None,
source_refname: None,
};
let all_branches: Vec<Branch> = vec![branch_a.clone(), branch_b.clone()];
let claim: Vec<OwnershipClaim> = vec![OwnershipClaim {

View File

@ -163,10 +163,8 @@ impl Oplog for Project {
let mut branches_tree_builder = repo.treebuilder(None)?;
let mut head_tree_ids = Vec::new();
for branch in vb_state.list_branches()? {
if branch.applied {
head_tree_ids.push(branch.tree);
}
for branch in vb_state.list_branches_in_workspace()? {
head_tree_ids.push(branch.tree);
// commits in virtual branches (tree and commit data)
// calculate all the commits between branch.head and the target and codify them
@ -713,12 +711,9 @@ fn lines_since_snapshot(project: &Project, repo: &git2::Repository) -> Result<us
return Ok(0);
};
let vbranches = project.virtual_branches().list_branches()?;
let vbranches = project.virtual_branches().list_branches_in_workspace()?;
let mut lines_changed = 0;
let dirty_branches = vbranches
.iter()
.filter(|b| b.applied)
.filter(|b| !b.ownership.claims.is_empty());
let dirty_branches = vbranches.iter().filter(|b| !b.ownership.claims.is_empty());
for branch in dirty_branches {
lines_changed += branch_lines_since_snapshot(branch, repo, oplog_commit_id)?;
}
@ -812,9 +807,8 @@ fn tree_from_applied_vbranches(
let vbs_from_toml: VirtualBranchesState = toml::from_str(from_utf8(vb_toml_blob.content())?)?;
let applied_branch_trees: Vec<git2::Oid> = vbs_from_toml
.branches
.values()
.filter(|b| b.applied)
.list_branches_in_workspace()?
.iter()
.map(|b| b.tree)
.collect();