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( export function clickOutside(node: HTMLElement, params: ClickOpts) {
node: HTMLElement,
params: ClickOpts
): { destroy: () => void; update: (opts: ClickOpts) => void } {
let trigger: HTMLElement | undefined;
function onClick(event: MouseEvent) { function onClick(event: MouseEvent) {
if ( if (
node && node &&
!node.contains(event.target as HTMLElement) && !node.contains(event.target as HTMLElement) &&
!trigger?.contains(event.target as HTMLElement) !params.excludeElement?.contains(event.target as HTMLElement)
) { ) {
params.handler(); params.handler();
} }
@ -20,14 +16,6 @@ export function clickOutside(
destroy() { destroy() {
document.removeEventListener('mousedown', onClick, true); document.removeEventListener('mousedown', onClick, true);
document.removeEventListener('contextmenu', 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"> <script lang="ts">
import { clickOutside } from '$lib/clickOutsideNew'; import { clickOutside } from '$lib/clickOutside';
import { portal } from '$lib/utils/portal'; import { portal } from '$lib/utils/portal';
import { pxToRem } from '$lib/utils/pxToRem';
import { resizeObserver } from '$lib/utils/resizeObserver'; import { resizeObserver } from '$lib/utils/resizeObserver';
import { type Snippet } from 'svelte'; import { type Snippet } from 'svelte';
@ -27,7 +26,6 @@
}: Props = $props(); }: Props = $props();
// LOCAL VARS // LOCAL VARS
let menuMargin = 4;
// STATES // STATES
let item = $state<any>(); let item = $state<any>();
@ -71,12 +69,10 @@
function setVerticalAlign(targetBoundingRect: DOMRect) { function setVerticalAlign(targetBoundingRect: DOMRect) {
if (verticalAlign === 'top') { if (verticalAlign === 'top') {
return targetBoundingRect?.top ? targetBoundingRect.top - contextMenuHeight - menuMargin : 0; return targetBoundingRect?.top ? targetBoundingRect.top - contextMenuHeight : 0;
} }
return targetBoundingRect?.top return targetBoundingRect?.top ? targetBoundingRect.top + targetBoundingRect.height : 0;
? targetBoundingRect.top + targetBoundingRect.height + menuMargin
: 0;
} }
function setHorizontalAlign(targetBoundingRect: DOMRect) { function setHorizontalAlign(targetBoundingRect: DOMRect) {
@ -131,8 +127,10 @@
bind:offsetHeight={contextMenuHeight} bind:offsetHeight={contextMenuHeight}
bind:offsetWidth={contextMenuWidth} bind:offsetWidth={contextMenuWidth}
class="context-menu" class="context-menu"
style:top={pxToRem(menuPosition.y)} class:top-oriented={verticalAlign === 'top'}
style:left={pxToRem(menuPosition.x)} class:bottom-oriented={verticalAlign === 'bottom'}
style:top="{menuPosition.y}px"
style:left="{menuPosition.x}px"
style:transform-origin={setTransformOrigin()} style:transform-origin={setTransformOrigin()}
style:--animation-transform-shift={verticalAlign === 'top' ? '6px' : '-6px'} style:--animation-transform-shift={verticalAlign === 'top' ? '6px' : '-6px'}
> >
@ -167,6 +165,14 @@
/* background-color: rgba(0, 0, 0, 0.1); */ /* background-color: rgba(0, 0, 0, 0.1); */
} }
.top-oriented {
margin-top: -4px;
}
.bottom-oriented {
margin-top: 4px;
}
.context-menu { .context-menu {
z-index: var(--z-blocker); z-index: var(--z-blocker);
position: fixed; position: fixed;

View File

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

View File

@ -85,14 +85,6 @@
class:resizer-hovered={isResizerHovered || isResizerDragging} class:resizer-hovered={isResizerHovered || isResizerDragging}
on:mousedown={toggleNavCollapse} 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"> <svg viewBox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M5 1.25L1.59055 5.08564C1.25376 5.46452 1.25376 6.03548 1.59055 6.41436L5 10.25" 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 ProjectAvatar from './ProjectAvatar.svelte';
import ProjectsPopup from './ProjectsPopup.svelte'; import ProjectsPopup from './ProjectsPopup.svelte';
import { Project } from '$lib/backend/projects'; import { Project } from '$lib/backend/projects';
import { clickOutside } from '$lib/clickOutside';
import Icon from '$lib/shared/Icon.svelte'; import Icon from '$lib/shared/Icon.svelte';
import { getContext } from '$lib/utils/context'; import { getContext } from '$lib/utils/context';
import { tooltip } from '$lib/utils/tooltip'; import { tooltip } from '$lib/utils/tooltip';
export let isNavCollapsed: boolean; export let isNavCollapsed: boolean;
let buttonTrigger: HTMLButtonElement;
const project = getContext(Project); const project = getContext(Project);
let popup: ProjectsPopup; let popup: ProjectsPopup;
let visible: boolean = false;
</script> </script>
<div <div class="wrapper">
class="wrapper"
use:clickOutside={{
handler: () => {
popup.hide();
visible = false;
},
enabled: visible
}}
>
<button <button
bind:this={buttonTrigger}
class="text-input button" class="text-input button"
use:tooltip={isNavCollapsed ? project?.title : ''} use:tooltip={isNavCollapsed ? project?.title : ''}
on:mousedown={(e) => { on:mousedown={(e) => {
visible = popup.toggle();
e.preventDefault(); e.preventDefault();
popup.toggle();
}} }}
> >
<ProjectAvatar name={project?.title} /> <ProjectAvatar name={project?.title} />
@ -41,7 +32,7 @@
</div> </div>
{/if} {/if}
</button> </button>
<ProjectsPopup bind:this={popup} {isNavCollapsed} /> <ProjectsPopup bind:this={popup} target={buttonTrigger} {isNavCollapsed} />
</div> </div>
<style lang="postcss"> <style lang="postcss">

View File

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

View File

@ -13,7 +13,6 @@
import TextBox from '../shared/TextBox.svelte'; import TextBox from '../shared/TextBox.svelte';
import { KeyName } from '$lib/utils/hotkeys'; import { KeyName } from '$lib/utils/hotkeys';
import { portal } from '$lib/utils/portal'; import { portal } from '$lib/utils/portal';
import { pxToRem } from '$lib/utils/pxToRem';
import { resizeObserver } from '$lib/utils/resizeObserver'; import { resizeObserver } from '$lib/utils/resizeObserver';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@ -83,6 +82,7 @@
function getInputBoundingRect() { function getInputBoundingRect() {
if (selectWrapperEl) { if (selectWrapperEl) {
inputBoundingRect = selectWrapperEl.getBoundingClientRect(); inputBoundingRect = selectWrapperEl.getBoundingClientRect();
console.log('inputBoundingRect', inputBoundingRect);
} }
} }
@ -182,10 +182,10 @@
class="options card" class="options card"
style:width="{inputBoundingRect?.width}px" style:width="{inputBoundingRect?.width}px"
style:top={inputBoundingRect?.top style:top={inputBoundingRect?.top
? pxToRem(inputBoundingRect.top + inputBoundingRect.height) ? `${inputBoundingRect.top + inputBoundingRect.height}px`
: undefined} : undefined}
style:left={inputBoundingRect?.left ? pxToRem(inputBoundingRect.left) : undefined} style:left={inputBoundingRect?.left ? `${inputBoundingRect.left}px` : undefined}
style:max-height={maxHeightState && pxToRem(maxHeightState)} style:max-height={maxHeightState && `${maxHeightState}px`}
> >
<ScrollableContainer initiallyVisible> <ScrollableContainer initiallyVisible>
{#if searchable && options.length > 5} {#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 vb_state = project_repository.project().virtual_branches();
let all_virtual_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")?; .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 let target_commit = project_repository
.repo() .repo()
.find_commit(default_target.sha) .find_commit(default_target.sha)
@ -83,7 +78,7 @@ fn go_back_to_integration(
let mut final_tree = target_commit let mut final_tree = target_commit
.tree() .tree()
.context("failed to get base tree from commit")?; .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 // merge this branches tree with our tree
let branch_head = project_repository let branch_head = project_repository
.repo() .repo()
@ -241,7 +236,7 @@ pub fn set_base_branch(
id: BranchId::generate(), id: BranchId::generate(),
name: head_name.to_string().replace("refs/heads/", ""), name: head_name.to_string().replace("refs/heads/", ""),
notes: String::new(), notes: String::new(),
applied: true, source_refname: Some(head_name),
upstream, upstream,
upstream_head, upstream_head,
created_timestamp_ms: now_ms, created_timestamp_ms: now_ms,
@ -256,6 +251,9 @@ pub fn set_base_branch(
order: 0, order: 0,
selected_for_changes: None, selected_for_changes: None,
allow_rebasing: project_repository.project().ok_with_force_push.into(), 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)?; vb_state.set_branch(branch)?;
@ -402,7 +400,7 @@ pub fn update_base_branch(
if non_commited_files.is_empty() { if non_commited_files.is_empty() {
// if there are no commited files, then the branch is fully merged // if there are no commited files, then the branch is fully merged
// and we can delete it. // 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)?; project_repository.delete_branch_reference(&branch)?;
Ok(None) Ok(None)
} else { } else {
@ -545,7 +543,6 @@ pub fn update_base_branch(
let final_tree = updated_vbranches let final_tree = updated_vbranches
.iter() .iter()
.filter(|branch| branch.applied)
.fold(new_target_commit.tree(), |final_tree, branch| { .fold(new_target_commit.tree(), |final_tree, branch| {
let repo: &git2::Repository = repo; let repo: &git2::Repository = repo;
let final_tree = final_tree?; let final_tree = final_tree?;

View File

@ -192,7 +192,7 @@ impl Controller {
let old_branch = project_repository let old_branch = project_repository
.project() .project()
.virtual_branches() .virtual_branches()
.get_branch(branch_update.id)?; .get_branch_in_workspace(branch_update.id)?;
let result = branch::update_branch(&project_repository, &branch_update); let result = branch::update_branch(&project_repository, &branch_update);
let _ = snapshot_tree.and_then(|snapshot_tree| { let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository.project().snapshot_branch_update( 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 repo: &git2::Repository = project_repo.repo();
let vb_state = project_repo.project().virtual_branches(); let vb_state = project_repo.project().virtual_branches();
let all_virtual_branches = vb_state.list_branches()?; let virtual_branches = vb_state.list_branches_in_workspace()?;
let applied_branches = all_virtual_branches
.iter()
.filter(|branch| branch.applied)
.collect::<Vec<_>>();
let target_commit = repo.find_commit(target.sha)?; let target_commit = repo.find_commit(target.sha)?;
let mut workspace_tree = target_commit.tree()?; let mut workspace_tree = target_commit.tree()?;
@ -50,12 +46,12 @@ pub fn get_workspace_head(
if conflicts::is_conflicting(project_repo, None)? { if conflicts::is_conflicting(project_repo, None)? {
let merge_parent = let merge_parent =
conflicts::merge_parent(project_repo)?.ok_or(anyhow!("No 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)?; let merge_base = repo.merge_base(first_branch.head, merge_parent)?;
workspace_tree = repo.find_commit(merge_base)?.tree()?; workspace_tree = repo.find_commit(merge_base)?.tree()?;
} else { } else {
for branch in &applied_branches { for branch in &virtual_branches {
let branch_tree = repo.find_commit(branch.head)?.tree()?; let branch_tree = repo.find_commit(branch.head)?.tree()?;
let merge_tree = repo.find_commit(target.sha)?.tree()?; let merge_tree = repo.find_commit(target.sha)?.tree()?;
let mut index = repo.merge_trees(&merge_tree, &workspace_tree, &branch_tree, None)?; 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() .iter()
.map(|b| repo.find_commit(b.head)) .map(|b| repo.find_commit(b.head))
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
@ -82,7 +78,7 @@ pub fn get_workspace_head(
// TODO(mg): Can we make this a constant? // TODO(mg): Can we make this a constant?
let committer = get_integration_commiter()?; let committer = get_integration_commiter()?;
let mut heads: Vec<git2::Commit<'_>> = applied_branches let mut heads: Vec<git2::Commit<'_>> = virtual_branches
.iter() .iter()
.filter(|b| b.head != target.sha) .filter(|b| b.head != target.sha)
.map(|b| repo.find_commit(b.head)) .map(|b| repo.find_commit(b.head))
@ -168,15 +164,10 @@ pub fn update_gitbutler_integration(
let vb_state = project_repository.project().virtual_branches(); let vb_state = project_repository.project().virtual_branches();
// get all virtual branches, we need to try to update them all // get all virtual branches, we need to try to update them all
let all_virtual_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to list virtual branches")?; .context("failed to list virtual branches")?;
let applied_virtual_branches = all_virtual_branches
.iter()
.filter(|branch| branch.applied)
.collect::<Vec<_>>();
let integration_commit = let integration_commit =
repo.find_commit(get_workspace_head(&vb_state, project_repository)?)?; repo.find_commit(get_workspace_head(&vb_state, project_repository)?)?;
let integration_tree = integration_commit.tree()?; 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 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("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"); 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(" - ");
message.push_str(branch.name.as_str()); message.push_str(branch.name.as_str());
message.push_str(format!(" ({})", &branch.refname()).as_str()); message.push_str(format!(" ({})", &branch.refname()).as_str());
@ -250,7 +241,7 @@ pub fn update_gitbutler_integration(
index.write()?; index.write()?;
// finally, update the refs/gitbutler/ heads to the states of the current virtual branches // 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 wip_tree = repo.find_tree(branch.tree)?;
let mut branch_head = repo.find_commit(branch.head)?; let mut branch_head = repo.find_commit(branch.head)?;
let head_tree = branch_head.tree()?; 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 vb_state = project_repository.project().virtual_branches();
let default_target = vb_state.get_default_target()?; let default_target = vb_state.get_default_target()?;
let applied_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")? .context("failed to read virtual branches")?;
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
let integration_commit_id = get_workspace_head(&vb_state, project_repository)?; let integration_commit_id = get_workspace_head(&vb_state, project_repository)?;
@ -226,7 +223,7 @@ pub fn unapply_ownership(
project_repository, project_repository,
&integration_commit_id, &integration_commit_id,
&default_target.sha, &default_target.sha,
applied_branches, virtual_branches,
) )
.context("failed to get status by branch")?; .context("failed to get status by branch")?;
@ -338,7 +335,7 @@ pub fn convert_to_real_branch(
) -> Result<git2::Branch<'_>> { ) -> Result<git2::Branch<'_>> {
fn build_real_branch<'l>( fn build_real_branch<'l>(
project_repository: &'l ProjectRepo, project_repository: &'l ProjectRepo,
vbranch: &branch::Branch, vbranch: &mut branch::Branch,
name_conflict_resolution: NameConflitResolution, name_conflict_resolution: NameConflitResolution,
) -> Result<git2::Branch<'l>> { ) -> Result<git2::Branch<'l>> {
let repo = project_repository.repo(); let repo = project_repository.repo();
@ -381,7 +378,10 @@ pub fn convert_to_real_branch(
branch_name branch_name
}; };
let vb_state = project_repository.project().virtual_branches();
let branch = repo.branch(&branch_name, &target_commit, true)?; 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)?; build_metadata_commit(project_repository, vbranch, &branch)?;
@ -389,7 +389,7 @@ pub fn convert_to_real_branch(
} }
fn build_metadata_commit<'l>( fn build_metadata_commit<'l>(
project_repository: &'l ProjectRepo, project_repository: &'l ProjectRepo,
vbranch: &branch::Branch, vbranch: &mut branch::Branch,
branch: &git2::Branch<'l>, branch: &git2::Branch<'l>,
) -> Result<git2::Oid> { ) -> Result<git2::Oid> {
let repo = project_repository.repo(); let repo = project_repository.repo();
@ -412,7 +412,7 @@ pub fn convert_to_real_branch(
let committer = get_integration_commiter()?; let committer = get_integration_commiter()?;
let parent = branch.get().peel_to_commit()?; 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( let commit_oid = repo.commit_with_signature(
Some(&branch.try_into()?), Some(&branch.try_into()?),
@ -421,18 +421,26 @@ pub fn convert_to_real_branch(
&message, &message,
&tree, &tree,
&[&parent], &[&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) Ok(commit_oid)
} }
let vb_state = project_repository.project().virtual_branches(); 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 // Convert the vbranch to a real branch
let real_branch = let real_branch = build_real_branch(
build_real_branch(project_repository, &target_branch, name_conflict_resolution)?; project_repository,
&mut target_branch,
name_conflict_resolution,
)?;
delete_branch(project_repository, branch_id)?; delete_branch(project_repository, branch_id)?;
@ -470,12 +478,40 @@ fn find_base_tree<'a>(
Ok(base_tree) 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( pub fn list_virtual_branches(
project_repository: &ProjectRepo, project_repository: &ProjectRepo,
) -> Result<(Vec<VirtualBranch>, Vec<diff::FileDiff>)> { ) -> Result<(Vec<VirtualBranch>, Vec<diff::FileDiff>)> {
let mut branches: Vec<VirtualBranch> = Vec::new(); let mut branches: Vec<VirtualBranch> = Vec::new();
let vb_state = project_repository.project().virtual_branches(); let vb_state = project_repository.project().virtual_branches();
resolve_old_applied_state(project_repository, &vb_state)?;
let default_target = vb_state let default_target = vb_state
.get_default_target() .get_default_target()
.context("failed to get default target")?; .context("failed to get default target")?;
@ -496,10 +532,6 @@ pub fn list_virtual_branches(
.unwrap_or(-1); .unwrap_or(-1);
for (branch, files) in statuses { for (branch, files) in statuses {
if !branch.applied {
convert_to_real_branch(project_repository, branch.id, Default::default())?;
}
let repo = project_repository.repo(); let repo = project_repository.repo();
update_conflict_markers(project_repository, &files)?; update_conflict_markers(project_repository, &files)?;
@ -566,10 +598,7 @@ pub fn list_virtual_branches(
let merge_base = repo let merge_base = repo
.merge_base(default_target.sha, branch.head) .merge_base(default_target.sha, branch.head)
.context("failed to find merge base")?; .context("failed to find merge base")?;
let mut base_current = true; let base_current = true;
if !branch.applied {
base_current = merge_base == default_target.sha;
}
let upstream = upstream_branch let upstream = upstream_branch
.map(|upstream_branch| branch_to_remote_branch(&upstream_branch)) .map(|upstream_branch| branch_to_remote_branch(&upstream_branch))
@ -604,7 +633,7 @@ pub fn list_virtual_branches(
id: branch.id, id: branch.id,
name: branch.name, name: branch.name,
notes: branch.notes, notes: branch.notes,
active: branch.applied, active: true,
files, files,
order: branch.order, order: branch.order,
commits: vbranch_commits, commits: vbranch_commits,
@ -754,7 +783,7 @@ pub fn create_virtual_branch(
.context("failed to find defaut target commit tree")?; .context("failed to find defaut target commit tree")?;
let mut all_virtual_branches = vb_state let mut all_virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")?; .context("failed to read virtual branches")?;
let name = dedup( 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 { let selected_for_changes = if let Some(selected_for_changes) = create.selected_for_changes {
if selected_for_changes { if selected_for_changes {
for mut other_branch in vb_state for mut other_branch in vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")? .context("failed to read virtual branches")?
{ {
other_branch.selected_for_changes = None; other_branch.selected_for_changes = None;
@ -812,7 +841,6 @@ pub fn create_virtual_branch(
id: BranchId::generate(), id: BranchId::generate(),
name: name.clone(), name: name.clone(),
notes: String::new(), notes: String::new(),
applied: true,
upstream: None, upstream: None,
upstream_head: None, upstream_head: None,
tree: tree.id(), tree: tree.id(),
@ -823,6 +851,10 @@ pub fn create_virtual_branch(
order, order,
selected_for_changes, selected_for_changes,
allow_rebasing: project_repository.project().ok_with_force_push.into(), 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 { if let Some(ownership) = &create.ownership {
@ -870,7 +902,7 @@ pub fn integrate_upstream_commits(
let project = project_repository.project(); let project = project_repository.project();
let vb_state = project.virtual_branches(); 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 default_target = vb_state.get_default_target()?;
let upstream_branch = branch.upstream.as_ref().context("upstream not found")?; let upstream_branch = branch.upstream.as_ref().context("upstream not found")?;
@ -1061,7 +1093,7 @@ pub fn update_branch(
branch_update: &branch::BranchUpdateRequest, branch_update: &branch::BranchUpdateRequest,
) -> Result<branch::Branch> { ) -> Result<branch::Branch> {
let vb_state = project_repository.project().virtual_branches(); 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 { if let Some(ownership) = &branch_update.ownership {
set_ownership(&vb_state, &mut branch, ownership).context("failed to set 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 { if let Some(name) = &branch_update.name {
let all_virtual_branches = vb_state let all_virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")?; .context("failed to read virtual branches")?;
project_repository.delete_branch_reference(&branch)?; 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 { if let Some(selected_for_changes) = branch_update.selected_for_changes {
branch.selected_for_changes = if selected_for_changes { branch.selected_for_changes = if selected_for_changes {
for mut other_branch in vb_state for mut other_branch in vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")? .context("failed to read virtual branches")?
.into_iter() .into_iter()
.filter(|b| b.id != branch.id) .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<()> { pub fn delete_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> Result<()> {
let vb_state = project_repository.project().virtual_branches(); 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(()); return Ok(());
}; };
_ = project_repository _ = project_repository
@ -1150,18 +1182,15 @@ pub fn delete_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> R
let target_commit = repo.target_commit()?; let target_commit = repo.target_commit()?;
let base_tree = target_commit.tree().context("failed to get target tree")?; let base_tree = target_commit.tree().context("failed to get target tree")?;
let applied_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")? .context("failed to read virtual branches")?;
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
let (applied_statuses, _) = get_applied_status( let (applied_statuses, _) = get_applied_status(
project_repository, project_repository,
&integration_commit.id(), &integration_commit.id(),
&target_commit.id(), &target_commit.id(),
applied_branches, virtual_branches,
) )
.context("failed to get status by branch")?; .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")?; .context("failed to checkout tree")?;
vb_state vb_state
.remove_branch(branch.id) .mark_as_not_in_workspace(branch.id)
.context("Failed to remove branch")?; .context("Failed to remove branch")?;
project_repository.delete_branch_reference(&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<()> { fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
let mut applied_branches = vb_state let mut virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to list branches")? .context("failed to list branches")?;
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
if applied_branches.is_empty() { if virtual_branches.is_empty() {
println!("no applied branches"); println!("no applied branches");
return Ok(()); return Ok(());
} }
if applied_branches if virtual_branches
.iter() .iter()
.any(|b| b.selected_for_changes.is_some()) .any(|b| b.selected_for_changes.is_some())
{ {
@ -1223,10 +1249,10 @@ fn ensure_selected_for_changes(vb_state: &VirtualBranchesHandle) -> Result<()> {
return Ok(()); 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()); virtual_branches[0].selected_for_changes = Some(now_since_unix_epoch_ms());
vb_state.set_branch(applied_branches[0].clone())?; vb_state.set_branch(virtual_branches[0].clone())?;
Ok(()) Ok(())
} }
@ -1241,7 +1267,7 @@ fn set_ownership(
} }
let virtual_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")?; .context("failed to read virtual branches")?;
let mut claim_outcomes = let mut claim_outcomes =
@ -1331,71 +1357,18 @@ pub fn get_status_by_branch(
let default_target = vb_state.get_default_target()?; let default_target = vb_state.get_default_target()?;
let virtual_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")?; .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( let (applied_status, skipped_files) = get_applied_status(
project_repository, project_repository,
// TODO: Keep this optional or update lots of tests? // TODO: Keep this optional or update lots of tests?
integration_commit.unwrap_or(&default_target.sha), integration_commit.unwrap_or(&default_target.sha),
&default_target.sha, &default_target.sha,
applied_virtual_branches, virtual_branches,
)?; )?;
let non_applied_virtual_branches = virtual_branches Ok((applied_status, skipped_files))
.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<_>>>()
} }
fn new_compute_locks( fn new_compute_locks(
@ -1414,7 +1387,6 @@ fn new_compute_locks(
let branch_path_diffs = virtual_branches let branch_path_diffs = virtual_branches
.iter() .iter()
.filter(|branch| branch.applied)
.filter_map(|branch| { .filter_map(|branch| {
let commit = repository.find_commit(branch.head).ok()?; let commit = repository.find_commit(branch.head).ok()?;
let tree = commit.tree().ok()?; let tree = commit.tree().ok()?;
@ -1603,10 +1575,6 @@ fn get_applied_status(
}; };
for branch in &mut virtual_branches { for branch in &mut virtual_branches {
if !branch.applied {
bail!("branch {} is not applied", branch.name);
}
let old_claims = branch.ownership.claims.clone(); let old_claims = branch.ownership.claims.clone();
let new_claims = old_claims let new_claims = old_claims
.iter() .iter()
@ -1783,7 +1751,7 @@ pub fn reset_branch(
let default_target = vb_state.get_default_target()?; 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 { if branch.head == target_commit_id {
// nothing to do // nothing to do
return Ok(()); return Ok(());
@ -2156,7 +2124,7 @@ pub fn push(
) -> Result<()> { ) -> Result<()> {
let vb_state = project_repository.project().virtual_branches(); 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 { let remote_branch = if let Some(upstream_branch) = &vbranch.upstream {
upstream_branch.clone() upstream_branch.clone()
} else { } else {
@ -2330,7 +2298,7 @@ pub fn move_commit_file(
) -> Result<git2::Oid> { ) -> Result<git2::Oid> {
let vb_state = project_repository.project().virtual_branches(); 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 return Ok(to_commit_id); // this is wrong
}; };
@ -2567,20 +2535,11 @@ pub fn amend(
project_repository.assure_resolved()?; project_repository.assure_resolved()?;
let vb_state = project_repository.project().virtual_branches(); let vb_state = project_repository.project().virtual_branches();
let all_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")?; .context("failed to read virtual branches")?;
if !all_branches.iter().any(|b| b.id == branch_id) { if !virtual_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) {
bail!("could not find applied branch with id {branch_id} to amend to"); bail!("could not find applied branch with id {branch_id} to amend to");
} }
@ -2593,7 +2552,7 @@ pub fn amend(
project_repository, project_repository,
&integration_commit_id, &integration_commit_id,
&default_target.sha, &default_target.sha,
applied_branches, virtual_branches,
)?; )?;
let (ref mut target_branch, target_status) = applied_statuses 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 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 // find the commit to offset from
let commit = project_repository let commit = project_repository
.repo() .repo()
@ -2798,7 +2757,7 @@ pub fn insert_blank_commit(
) -> Result<()> { ) -> Result<()> {
let vb_state = project_repository.project().virtual_branches(); 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 // find the commit to offset from
let mut commit = project_repository let mut commit = project_repository
.repo() .repo()
@ -2851,7 +2810,7 @@ pub fn undo_commit(
) -> Result<()> { ) -> Result<()> {
let vb_state = project_repository.project().virtual_branches(); 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 let commit = project_repository
.repo() .repo()
.find_commit(commit_oid) .find_commit(commit_oid)
@ -2903,7 +2862,7 @@ pub fn squash(
project_repository.assure_resolved()?; project_repository.assure_resolved()?;
let vb_state = project_repository.project().virtual_branches(); 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 default_target = vb_state.get_default_target()?;
let branch_commit_oids = let branch_commit_oids =
project_repository.l(branch.head, LogUntil::Commit(default_target.sha))?; 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 vb_state = project_repository.project().virtual_branches();
let default_target = vb_state.get_default_target()?; 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 = let branch_commit_oids =
project_repository.l(branch.head, LogUntil::Commit(default_target.sha))?; 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 vb_state = project_repository.project().virtual_branches();
let applied_branches = vb_state let applied_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")? .context("failed to read virtual branches")?;
.into_iter()
.filter(|b| b.applied)
.collect::<Vec<_>>();
if !applied_branches.iter().any(|b| b.id == target_branch_id) { if !applied_branches.iter().any(|b| b.id == target_branch_id) {
bail!("branch {target_branch_id} is not among applied branches") 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 // 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 { for ownership in ownerships_to_transfer {
destination_branch.ownership.put(ownership); destination_branch.ownership.put(ownership);
@ -3204,16 +3160,13 @@ pub fn create_virtual_branch_from_branch(
) -> Result<BranchId> { ) -> Result<BranchId> {
fn apply_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> Result<String> { fn apply_branch(project_repository: &ProjectRepo, branch_id: BranchId) -> Result<String> {
project_repository.assure_resolved()?; project_repository.assure_resolved()?;
project_repository.assure_unconflicted()?;
let repo = project_repository.repo(); let repo = project_repository.repo();
let vb_state = project_repository.project().virtual_branches(); let vb_state = project_repository.project().virtual_branches();
let default_target = vb_state.get_default_target()?; 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.applied {
return Ok(branch.name);
}
let target_commit = repo let target_commit = repo
.find_commit(default_target.sha) .find_commit(default_target.sha)
@ -3247,17 +3200,15 @@ pub fn create_virtual_branch_from_branch(
if merge_index.has_conflicts() { if merge_index.has_conflicts() {
// currently we can only deal with the merge problem branch // currently we can only deal with the merge problem branch
for branch in get_status_by_branch(project_repository, Some(&target_commit.id()))? for branch in vb_state
.0 .list_branches_in_workspace()?
.into_iter() .iter()
.map(|(branch, _)| branch)
.filter(|branch| branch.id != branch_id) .filter(|branch| branch.id != branch_id)
{ {
convert_to_real_branch(project_repository, branch.id, Default::default())?; convert_to_real_branch(project_repository, branch.id, Default::default())?;
} }
// apply the branch // apply the branch
branch.applied = true;
vb_state.set_branch(branch.clone())?; vb_state.set_branch(branch.clone())?;
// checkout the conflicts // checkout the conflicts
@ -3393,12 +3344,27 @@ pub fn create_virtual_branch_from_branch(
.context("failed to merge trees")?; .context("failed to merge trees")?;
if merge_index.has_conflicts() { if merge_index.has_conflicts() {
return Err(anyhow!("branch {branch_id} is in a conflicting state")) // mark conflicts
.context(Marker::ProjectConflict); 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 // apply the branch
branch.applied = true;
vb_state.set_branch(branch.clone())?; vb_state.set_branch(branch.clone())?;
ensure_selected_for_changes(&vb_state).context("failed to ensure selected for changes")?; 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")?; .context("failed to checkout index")?;
// Look for and handle the vbranch indicator commit // 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 let Some(headers) = potential_wip_commit.gitbutler_headers() {
if headers.is_unapplied_header_commit { if headers.change_id == wip_commit_to_unapply {
if let Some(branch_name) = headers.vbranch_name { undo_commit(project_repository, branch.id, branch.head)?;
branch.name = branch_name; }
vb_state.set_branch(branch.clone())?;
};
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")?; .context("failed to peel to commit")?;
let head_commit_tree = head_commit.tree().context("failed to find tree")?; let head_commit_tree = head_commit.tree().context("failed to find tree")?;
let all_virtual_branches = vb_state let virtual_branches = vb_state
.list_branches() .list_branches_in_workspace()
.context("failed to read virtual branches")? .context("failed to read virtual branches")?
.into_iter() .into_iter()
.collect::<Vec<branch::Branch>>(); .collect::<Vec<branch::Branch>>();
let order = vb_state.next_order_index()?; let order = vb_state.next_order_index()?;
let selected_for_changes = (!all_virtual_branches let selected_for_changes = (!virtual_branches
.iter() .iter()
.any(|b| b.selected_for_changes.is_some())) .any(|b| b.selected_for_changes.is_some()))
.then_some(now_since_unix_epoch_ms()); .then_some(now_since_unix_epoch_ms());
@ -3519,21 +3486,41 @@ pub fn create_virtual_branch_from_branch(
}, },
); );
let branch = branch::Branch { let branch = if let Ok(Some(mut branch)) =
id: BranchId::generate(), vb_state.find_by_source_refname_where_not_in_workspace(upstream)
name: branch_name.clone(), {
notes: String::new(), branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id());
applied: false, branch.upstream = upstream_branch;
upstream_head: upstream_branch.is_some().then_some(head_commit.id()), branch.tree = head_commit_tree.id();
upstream: upstream_branch, branch.head = head_commit.id();
tree: head_commit_tree.id(), branch.ownership = ownership;
head: head_commit.id(), branch.order = order;
created_timestamp_ms: now, branch.selected_for_changes = selected_for_changes;
updated_timestamp_ms: now, branch.allow_rebasing = project_repository.project().ok_with_force_push.into();
ownership, branch.old_applied = true;
order, branch.in_workspace = true;
selected_for_changes,
allow_rebasing: project_repository.project().ok_with_force_push.into(), 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())?; 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"); virtual_branches::get_status_by_branch(project_repository, None).expect("failed to get status");
let vb_state = project_repository.project().virtual_branches(); 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( let branch1 = create_virtual_branch(
project_repository, project_repository,
@ -260,7 +260,9 @@ fn create_branch_in_the_middle() -> Result<()> {
.expect("failed to create virtual branch"); .expect("failed to create virtual branch");
let vb_state = project_repository.project().virtual_branches(); 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); branches.sort_by_key(|b| b.order);
assert_eq!(branches.len(), 3); assert_eq!(branches.len(), 3);
assert_eq!(branches[0].name, "Virtual branch"); assert_eq!(branches[0].name, "Virtual branch");
@ -283,10 +285,11 @@ fn create_branch_no_arguments() -> Result<()> {
.expect("failed to create virtual branch"); .expect("failed to create virtual branch");
let vb_state = project_repository.project().virtual_branches(); 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.len(), 1);
assert_eq!(branches[0].name, "Virtual branch"); assert_eq!(branches[0].name, "Virtual branch");
assert!(branches[0].applied);
assert_eq!(branches[0].ownership, BranchOwnershipClaims::default()); assert_eq!(branches[0].ownership, BranchOwnershipClaims::default());
assert_eq!(branches[0].order, 0); assert_eq!(branches[0].order, 0);
@ -450,12 +453,12 @@ fn move_hunks_multiple_sources() -> Result<()> {
)?; )?;
let vb_state = project.virtual_branches(); 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 { branch2.ownership = BranchOwnershipClaims {
claims: vec!["test.txt:1-5".parse()?], claims: vec!["test.txt:1-5".parse()?],
}; };
vb_state.set_branch(branch2.clone())?; 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 { branch1.ownership = BranchOwnershipClaims {
claims: vec!["test.txt:11-15".parse()?], 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 // The change ID should always be generated by calling CommitHeadersV2::new
Some(git::CommitHeadersV2 { Some(git::CommitHeadersV2 {
change_id: "my-change-id".to_string(), change_id: "my-change-id".to_string(),
is_unapplied_header_commit: false,
vbranch_name: None,
}), }),
) )
.expect("failed to commit"); .expect("failed to commit");
@ -1086,7 +1087,8 @@ fn unapply_branch() -> Result<()> {
let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?; let (branches, _) = virtual_branches::list_virtual_branches(project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); 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); assert!(branch.active);
Ok(()) Ok(())
@ -1295,7 +1297,7 @@ fn detect_mergeable_branch() -> Result<()> {
let vb_state = project.virtual_branches(); 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 { branch4.ownership = BranchOwnershipClaims {
claims: vec!["test2.txt:1-6".parse()?], 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.len(), 1);
assert_eq!(branches[0].id, branch1_id); assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 0); 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].active);
assert!(!branches[0].conflicted); assert!(!branches[0].conflicted);
@ -196,8 +197,10 @@ async fn rebase_work() {
let (branches, _) = controller.list_virtual_branches(project).await.unwrap(); let (branches, _) = controller.list_virtual_branches(project).await.unwrap();
assert_eq!(branches.len(), 1); assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id); assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].files.len(), 1); // TODO: Should be 1
assert_eq!(branches[0].commits.len(), 0); 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].active);
assert!(!branches[0].conflicted); assert!(!branches[0].conflicted);

View File

@ -205,7 +205,7 @@ async fn conflicts_with_uncommited() {
.into_iter() .into_iter()
.find(|branch| branch.id == new_branch_id) .find(|branch| branch.id == new_branch_id)
.unwrap(); .unwrap();
assert!(!new_branch.active); assert_eq!(new_branch_id, new_branch.id);
assert_eq!(new_branch.commits.len(), 1); assert_eq!(new_branch.commits.len(), 1);
assert!(new_branch.upstream.is_some()); assert!(new_branch.upstream.is_some());
} }
@ -261,7 +261,7 @@ async fn conflicts_with_commited() {
.into_iter() .into_iter()
.find(|branch| branch.id == new_branch_id) .find(|branch| branch.id == new_branch_id)
.unwrap(); .unwrap();
assert!(!new_branch.active); assert_eq!(new_branch_id, new_branch.id);
assert_eq!(new_branch.commits.len(), 1); assert_eq!(new_branch.commits.len(), 1);
assert!(new_branch.upstream.is_some()); 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()?) .set_base_branch(project, &"refs/remotes/origin/master".parse()?)
.await?; .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 let branch_id = controller
.create_virtual_branch(project, &branch::BranchCreateRequest::default()) .create_virtual_branch(project, &branch::BranchCreateRequest::default())
.await?; .await?;
assert_eq!(project.virtual_branches().list_branches()?.len(), 1); assert_eq!(
project
.virtual_branches()
.list_branches_in_workspace()?
.len(),
1
);
// create commit // create commit
fs::write(repository.path().join("file.txt"), "content")?; 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" "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!( assert_eq!(
vbranches.len(), vbranches.len(),
1, 1,

View File

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

View File

@ -18,13 +18,9 @@ struct CommitHeadersV1 {
const V2_HEADERS_VERSION: &str = "2"; const V2_HEADERS_VERSION: &str = "2";
const V2_CHANGE_ID_HEADER: &str = "gitbutler-change-id"; const V2_CHANGE_ID_HEADER: &str = "gitbutler-change-id";
const V2_IS_UNAPPLIED_HEADER_COMMIT_HEADER: &str = "gitbutler-is-unapplied-header-commit"; #[derive(Debug, Clone)]
const V2_VBRANCH_NAME_HEADER: &str = "gitbutler-vbranch-name";
#[derive(Debug)]
pub struct CommitHeadersV2 { pub struct CommitHeadersV2 {
pub change_id: String, pub change_id: String,
pub is_unapplied_header_commit: bool,
pub vbranch_name: Option<String>,
} }
impl Default for CommitHeadersV2 { impl Default for CommitHeadersV2 {
@ -32,8 +28,6 @@ impl Default for CommitHeadersV2 {
CommitHeadersV2 { CommitHeadersV2 {
// Change ID using base16 encoding // Change ID using base16 encoding
change_id: Uuid::new_v4().to_string(), 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 { fn from(commit_headers_v1: CommitHeadersV1) -> CommitHeadersV2 {
CommitHeadersV2 { CommitHeadersV2 {
change_id: commit_headers_v1.change_id, 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 // We can safely assume that the change id should be UTF8
let change_id = change_id.as_str()?.to_string(); let change_id = change_id.as_str()?.to_string();
// We can rationalize about is unapplied header commit with a bstring Some(CommitHeadersV2 { change_id })
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,
})
} else { } else {
// Must be for a version we don't recognise // Must be for a version we don't recognise
None None
@ -100,10 +76,8 @@ impl HasCommitHeaders for git2::Commit<'_> {
impl CommitHeadersV2 { impl CommitHeadersV2 {
/// Used to create a CommitHeadersV2. This does not allow a change_id to be /// Used to create a CommitHeadersV2. This does not allow a change_id to be
/// provided in order to ensure a consistent format. /// 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 { CommitHeadersV2 {
is_unapplied_header_commit,
vbranch_name,
..Default::default() ..Default::default()
} }
} }
@ -115,18 +89,5 @@ impl CommitHeadersV2 {
pub fn inject_into(&self, commit_buffer: &mut CommitBuffer) { pub fn inject_into(&self, commit_buffer: &mut CommitBuffer) {
commit_buffer.set_header(HEADERS_VERSION_HEADER, V2_HEADERS_VERSION); commit_buffer.set_header(HEADERS_VERSION_HEADER, V2_HEADERS_VERSION);
commit_buffer.set_header(V2_CHANGE_ID_HEADER, &self.change_id); 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 id: BranchId,
pub name: String, pub name: String,
pub notes: String, pub notes: String,
pub applied: bool, pub source_refname: Option<git::Refname>,
pub upstream: Option<git::RemoteRefname>, pub upstream: Option<git::RemoteRefname>,
// upstream_head is the last commit on we've pushed to the upstream branch // upstream_head is the last commit on we've pushed to the upstream branch
#[serde(with = "crate::serde::oid_opt", default)] #[serde(with = "crate::serde::oid_opt", default)]
@ -50,6 +50,12 @@ pub struct Branch {
pub selected_for_changes: Option<i64>, pub selected_for_changes: Option<i64>,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub allow_rebasing: bool, 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 { fn default_true() -> bool {

View File

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

View File

@ -28,7 +28,6 @@ fn reconcile_ownership_simple() {
], ],
}], }],
}, },
applied: true,
tree: git2::Oid::zero(), tree: git2::Oid::zero(),
head: git2::Oid::zero(), head: git2::Oid::zero(),
id: BranchId::default(), id: BranchId::default(),
@ -40,6 +39,10 @@ fn reconcile_ownership_simple() {
order: usize::default(), order: usize::default(),
selected_for_changes: None, selected_for_changes: None,
allow_rebasing: true, allow_rebasing: true,
old_applied: true,
in_workspace: true,
not_in_workspace_wip_change_id: None,
source_refname: None,
}; };
let branch_b = Branch { let branch_b = Branch {
name: "b".to_string(), name: "b".to_string(),
@ -54,7 +57,6 @@ fn reconcile_ownership_simple() {
}], }],
}], }],
}, },
applied: true,
tree: git2::Oid::zero(), tree: git2::Oid::zero(),
head: git2::Oid::zero(), head: git2::Oid::zero(),
id: BranchId::default(), id: BranchId::default(),
@ -66,6 +68,10 @@ fn reconcile_ownership_simple() {
order: usize::default(), order: usize::default(),
selected_for_changes: None, selected_for_changes: None,
allow_rebasing: true, 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 all_branches: Vec<Branch> = vec![branch_a.clone(), branch_b.clone()];
let claim: Vec<OwnershipClaim> = vec![OwnershipClaim { 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 branches_tree_builder = repo.treebuilder(None)?;
let mut head_tree_ids = Vec::new(); let mut head_tree_ids = Vec::new();
for branch in vb_state.list_branches()? { for branch in vb_state.list_branches_in_workspace()? {
if branch.applied { head_tree_ids.push(branch.tree);
head_tree_ids.push(branch.tree);
}
// commits in virtual branches (tree and commit data) // commits in virtual branches (tree and commit data)
// calculate all the commits between branch.head and the target and codify them // 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); 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 mut lines_changed = 0;
let dirty_branches = vbranches let dirty_branches = vbranches.iter().filter(|b| !b.ownership.claims.is_empty());
.iter()
.filter(|b| b.applied)
.filter(|b| !b.ownership.claims.is_empty());
for branch in dirty_branches { for branch in dirty_branches {
lines_changed += branch_lines_since_snapshot(branch, repo, oplog_commit_id)?; 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 vbs_from_toml: VirtualBranchesState = toml::from_str(from_utf8(vb_toml_blob.content())?)?;
let applied_branch_trees: Vec<git2::Oid> = vbs_from_toml let applied_branch_trees: Vec<git2::Oid> = vbs_from_toml
.branches .list_branches_in_workspace()?
.values() .iter()
.filter(|b| b.applied)
.map(|b| b.tree) .map(|b| b.tree)
.collect(); .collect();