Merge pull request #4717 from gitbutlerapp/Rebase-revolution

Rebase revolution
This commit is contained in:
Caleb Owens 2024-08-22 18:28:49 +02:00 committed by GitHub
commit ec7a38f538
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 621 additions and 122 deletions

1
Cargo.lock generated
View File

@ -2422,6 +2422,7 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]

View File

@ -29,6 +29,14 @@ export class Project {
use_new_locking!: boolean; use_new_locking!: boolean;
ignore_project_semaphore!: boolean; ignore_project_semaphore!: boolean;
private succeeding_rebases!: boolean;
get succeedingRebases() {
return this.succeeding_rebases;
}
set succeedingRebases(value) {
this.succeeding_rebases = value;
}
// Produced just for the frontend to determine if the project is open in any window. // Produced just for the frontend to determine if the project is open in any window.
is_open!: boolean; is_open!: boolean;

View File

@ -10,7 +10,12 @@ class BranchDragActions {
) {} ) {}
acceptMoveCommit(data: any) { acceptMoveCommit(data: any) {
return data instanceof DraggableCommit && data.branchId !== this.branch.id && data.isHeadCommit; return (
data instanceof DraggableCommit &&
data.branchId !== this.branch.id &&
data.isHeadCommit &&
!data.commit.conflicted
);
} }
onMoveCommit(data: DraggableCommit) { onMoveCommit(data: DraggableCommit) {

View File

@ -4,7 +4,6 @@
import { BaseBranch } from '$lib/baseBranch/baseBranch'; import { BaseBranch } from '$lib/baseBranch/baseBranch';
import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte'; import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte';
import { persistedCommitMessage } from '$lib/config/config'; import { persistedCommitMessage } from '$lib/config/config';
import { featureEditMode } from '$lib/config/uiFeatureFlags';
import { draggableCommit } from '$lib/dragging/draggable'; import { draggableCommit } from '$lib/dragging/draggable';
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables'; import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
import BranchFilesList from '$lib/file/BranchFilesList.svelte'; import BranchFilesList from '$lib/file/BranchFilesList.svelte';
@ -44,8 +43,6 @@
const project = getContext(Project); const project = getContext(Project);
const modeService = maybeGetContext(ModeService); const modeService = maybeGetContext(ModeService);
const editModeEnabled = featureEditMode();
const commitStore = createCommitStore(commit); const commitStore = createCommitStore(commit);
$: commitStore.set(commit); $: commitStore.set(commit);
@ -135,6 +132,8 @@
modeService!.enterEditMode(commit.id, branch!.refname); modeService!.enterEditMode(commit.id, branch!.refname);
} }
$: conflicted = commit instanceof DetailedCommit && commit.conflicted;
</script> </script>
<Modal bind:this={commitMessageModal} width="small"> <Modal bind:this={commitMessageModal} width="small">
@ -270,6 +269,21 @@
<span class="commit__subtitle-divider"></span> <span class="commit__subtitle-divider"></span>
{/if} {/if}
{#if conflicted}
<div
class="commit__conflicted"
use:tooltip={{
text: 'Conflicted commits must be resolved before they can be ammended or squashed.\n\nPlease resolve conflicts using the "Resolve conflicts" button'
}}
>
<Icon name="warning-small" />
Conflicted
</div>
<span class="commit__subtitle-divider"></span>
{/if}
<button <button
class="commit__subtitle-btn commit__subtitle-btn_dashed" class="commit__subtitle-btn commit__subtitle-btn_dashed"
on:click|stopPropagation={() => copyToClipboard(commit.id)} on:click|stopPropagation={() => copyToClipboard(commit.id)}
@ -318,6 +332,7 @@
{#if isUndoable} {#if isUndoable}
<div class="commit__actions hide-native-scrollbar"> <div class="commit__actions hide-native-scrollbar">
{#if isUndoable} {#if isUndoable}
{#if !conflicted}
<Button <Button
size="tag" size="tag"
style="ghost" style="ghost"
@ -329,6 +344,7 @@
undoCommit(commit); undoCommit(commit);
}}>Undo</Button }}>Undo</Button
> >
{/if}
<Button <Button
size="tag" size="tag"
style="ghost" style="ghost"
@ -337,8 +353,14 @@
onclick={openCommitMessageModal}>Edit message</Button onclick={openCommitMessageModal}>Edit message</Button
> >
{/if} {/if}
{#if canEdit() && $editModeEnabled} {#if canEdit() && project.succeedingRebases}
<Button size="tag" style="ghost" outline onclick={editPatch}>Edit patch</Button> <Button size="tag" style="ghost" outline onclick={editPatch}>
{#if conflicted}
Resolve conflicts
{:else}
Edit patch
{/if}
</Button>
{/if} {/if}
</div> </div>
{/if} {/if}
@ -394,6 +416,14 @@
} }
} }
.commit__conflicted {
display: flex;
align-items: center;
gap: 4px;
color: var(--clr-core-err-40);
}
.accent-border-line { .accent-border-line {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -116,6 +116,8 @@
if (isLast) return 0; if (isLast) return 0;
return 0; return 0;
} }
$: localCommitsConflicted = $localCommits.some((commit) => commit.conflicted);
</script> </script>
{#snippet reorderDropzone(dropzone: ReorderDropzone, yOffsetPx: number)} {#snippet reorderDropzone(dropzone: ReorderDropzone, yOffsetPx: number)}
@ -225,6 +227,10 @@
kind="solid" kind="solid"
wide wide
loading={isPushingCommits} loading={isPushingCommits}
disabled={localCommitsConflicted}
help={localCommitsConflicted
? 'In order to push, please resolve any conflicted commits.'
: undefined}
onclick={async () => { onclick={async () => {
isPushingCommits = true; isPushingCommits = true;
try { try {

View File

@ -34,13 +34,15 @@ export class CommitDragActions {
if ( if (
data instanceof DraggableHunk && data instanceof DraggableHunk &&
data.branchId === this.branch.id && data.branchId === this.branch.id &&
data.commitId !== this.commit.id data.commitId !== this.commit.id &&
!this.commit.conflicted
) { ) {
return true; return true;
} else if ( } else if (
data instanceof DraggableFile && data instanceof DraggableFile &&
data.branchId === this.branch.id && data.branchId === this.branch.id &&
data.commit?.id !== this.commit.id data.commit?.id !== this.commit.id &&
!this.commit.conflicted
) { ) {
return true; return true;
} else { } else {
@ -79,6 +81,8 @@ export class CommitDragActions {
if (!(data instanceof DraggableCommit)) return false; if (!(data instanceof DraggableCommit)) return false;
if (data.branchId !== this.branch.id) return false; if (data.branchId !== this.branch.id) return false;
if (this.commit.conflicted || data.commit.conflicted) return false;
if (data.commit.isParentOf(this.commit)) { if (data.commit.isParentOf(this.commit)) {
if (data.commit.isIntegrated) return false; if (data.commit.isIntegrated) return false;
if (data.commit.isRemote && !this.project.ok_with_force_push) return false; if (data.commit.isRemote && !this.project.ok_with_force_push) return false;

View File

@ -38,7 +38,10 @@
</span> </span>
</p> </p>
<p class="switchrepo__message text-13 text-body">Bla bla bla</p> <p class="switchrepo__message text-13 text-body">
Please do not make any commits whilst in edit mode. To leave edit mode, use the "save changes"
button.
</p>
<div class="switchrepo__actions"> <div class="switchrepo__actions">
<Button <Button

View File

@ -15,8 +15,3 @@ export function featureInlineUnifiedDiffs(): Persisted<boolean> {
const key = 'inlineUnifiedDiffs'; const key = 'inlineUnifiedDiffs';
return persisted(false, key); return persisted(false, key);
} }
export function featureEditMode(): Persisted<boolean> {
const key = 'editMode';
return persisted(false, key);
}

View File

@ -71,6 +71,12 @@
let loading = true; let loading = true;
let signCheckResult = false; let signCheckResult = false;
let errorMessage = ''; let errorMessage = '';
let succeedingRebases = project.succeedingRebases;
$: {
project.succeedingRebases = succeedingRebases;
projectService.updateProject(project);
}
async function checkSigning() { async function checkSigning() {
checked = true; checked = true;
@ -308,4 +314,16 @@
<Toggle id="ignoreProjectSemaphore" bind:checked={ignoreProjectSemaphore} /> <Toggle id="ignoreProjectSemaphore" bind:checked={ignoreProjectSemaphore} />
</svelte:fragment> </svelte:fragment>
</SectionCard> </SectionCard>
<SectionCard labelFor="succeedingRebases" orientation="row">
<svelte:fragment slot="title">Edit mode and succeeding rebases</svelte:fragment>
<svelte:fragment slot="caption">
This is an experimental setting which will ensure that rebasing will always succeed,
introduces a mode for editing individual commits, and adds the ability to resolve conflicted
commits.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle id="succeedingRebases" bind:checked={succeedingRebases} />
</svelte:fragment>
</SectionCard>
</Section> </Section>

View File

@ -172,6 +172,7 @@ export class DetailedCommit {
changeId!: string; changeId!: string;
isSigned!: boolean; isSigned!: boolean;
relatedTo?: Commit; relatedTo?: Commit;
conflicted!: boolean;
prev?: DetailedCommit; prev?: DetailedCommit;
next?: DetailedCommit; next?: DetailedCommit;

View File

@ -2,7 +2,6 @@
import SectionCard from '$lib/components/SectionCard.svelte'; import SectionCard from '$lib/components/SectionCard.svelte';
import { import {
featureBaseBranchSwitching, featureBaseBranchSwitching,
featureEditMode,
featureInlineUnifiedDiffs featureInlineUnifiedDiffs
} from '$lib/config/uiFeatureFlags'; } from '$lib/config/uiFeatureFlags';
import ContentWrapper from '$lib/settings/ContentWrapper.svelte'; import ContentWrapper from '$lib/settings/ContentWrapper.svelte';
@ -10,7 +9,6 @@
const baseBranchSwitching = featureBaseBranchSwitching(); const baseBranchSwitching = featureBaseBranchSwitching();
const inlineUnifiedDiffs = featureInlineUnifiedDiffs(); const inlineUnifiedDiffs = featureInlineUnifiedDiffs();
const editMode = featureEditMode();
</script> </script>
<ContentWrapper title="Experimental features"> <ContentWrapper title="Experimental features">
@ -47,20 +45,6 @@
/> />
</svelte:fragment> </svelte:fragment>
</SectionCard> </SectionCard>
<SectionCard labelFor="editMode" orientation="row">
<svelte:fragment slot="title">Edit mode</svelte:fragment>
<svelte:fragment slot="caption">
Provides an "Edit patch" button on each commit which puts you into edit mode. Edit mode checks
out a particular commit so you can make changes to a particular commit, and then have the
child commits automatically rebased on top of the new changes.
<br /><br />
Please note that creating conflicts whilst inside edit mode is currently not supported. This feature
is still experimental and may result in loss of work.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle id="editMode" checked={$editMode} on:click={() => ($editMode = !$editMode)} />
</svelte:fragment>
</SectionCard>
</ContentWrapper> </ContentWrapper>
<style> <style>

View File

@ -360,8 +360,7 @@ pub(crate) fn update_base_branch(
let updated_vbranches = get_applied_status(ctx, None)? let updated_vbranches = get_applied_status(ctx, None)?
.branches .branches
.into_iter() .into_iter()
.map(|(branch, _)| branch) .map(|(mut branch, _)| -> Result<Option<Branch>> {
.map(|mut branch: Branch| -> Result<Option<Branch>> {
let branch_tree = repo.find_tree(branch.tree)?; let branch_tree = repo.find_tree(branch.tree)?;
let branch_head_commit = repo.find_commit(branch.head).context(format!( let branch_head_commit = repo.find_commit(branch.head).context(format!(

View File

@ -33,6 +33,7 @@ pub struct VirtualBranchCommit {
pub branch_id: BranchId, pub branch_id: BranchId,
pub change_id: Option<String>, pub change_id: Option<String>,
pub is_signed: bool, pub is_signed: bool,
pub conflicted: bool,
} }
pub(crate) fn commit_to_vbranch_commit( pub(crate) fn commit_to_vbranch_commit(
@ -68,6 +69,7 @@ pub(crate) fn commit_to_vbranch_commit(
branch_id: branch.id, branch_id: branch.id,
change_id: commit.change_id(), change_id: commit.change_id(),
is_signed: commit.is_signed(), is_signed: commit.is_signed(),
conflicted: commit.is_conflicted(),
}; };
Ok(commit) Ok(commit)

View File

@ -6,6 +6,7 @@ use std::{
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use gitbutler_command_context::CommandContext; use gitbutler_command_context::CommandContext;
use gitbutler_diff::FileDiff; use gitbutler_diff::FileDiff;
use gitbutler_repo::RepositoryExt;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@ -38,8 +39,12 @@ pub(crate) fn list_remote_commit_files(
} }
let parent = commit.parent(0).context("failed to get parent commit")?; let parent = commit.parent(0).context("failed to get parent commit")?;
let commit_tree = commit.tree().context("failed to get commit tree")?; let commit_tree = repository
let parent_tree = parent.tree().context("failed to get parent tree")?; .find_real_tree(&commit, Default::default())
.context("failed to get commit tree")?;
let parent_tree = repository
.find_real_tree(&parent, Default::default())
.context("failed to get parent tree")?;
let diff_files = gitbutler_diff::trees(repository, &parent_tree, &commit_tree)?; let diff_files = gitbutler_diff::trees(repository, &parent_tree, &commit_tree)?;
Ok(diff_files Ok(diff_files
@ -93,8 +98,13 @@ pub(crate) fn list_virtual_commit_files(
return Ok(vec![]); return Ok(vec![]);
} }
let parent = commit.parent(0).context("failed to get parent commit")?; let parent = commit.parent(0).context("failed to get parent commit")?;
let commit_tree = commit.tree().context("failed to get commit tree")?; let repository = ctx.repository();
let parent_tree = parent.tree().context("failed to get parent tree")?; let commit_tree = repository
.find_real_tree(commit, Default::default())
.context("failed to get commit tree")?;
let parent_tree = repository
.find_real_tree(&parent, Default::default())
.context("failed to get parent tree")?;
let diff = gitbutler_diff::trees(ctx.repository(), &parent_tree, &commit_tree)?; let diff = gitbutler_diff::trees(ctx.repository(), &parent_tree, &commit_tree)?;
let hunks_by_filepath = virtual_hunks_by_file_diffs(&ctx.project().path, diff); let hunks_by_filepath = virtual_hunks_by_file_diffs(&ctx.project().path, diff);
Ok(virtual_hunks_into_virtual_files(ctx, hunks_by_filepath)) Ok(virtual_hunks_into_virtual_files(ctx, hunks_by_filepath))

View File

@ -31,7 +31,7 @@ pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result<git2::Oid> {
let mut virtual_branches: Vec<Branch> = vb_state.list_branches_in_workspace()?; let mut virtual_branches: Vec<Branch> = vb_state.list_branches_in_workspace()?;
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 = repo.find_real_tree(&target_commit, Default::default())?;
if conflicts::is_conflicting(ctx, None)? { if conflicts::is_conflicting(ctx, None)? {
let merge_parent = conflicts::merge_parent(ctx)?.ok_or(anyhow!("No merge parent"))?; let merge_parent = conflicts::merge_parent(ctx)?.ok_or(anyhow!("No merge parent"))?;
@ -41,8 +41,10 @@ pub(crate) fn get_workspace_head(ctx: &CommandContext) -> Result<git2::Oid> {
workspace_tree = repo.find_commit(merge_base)?.tree()?; workspace_tree = repo.find_commit(merge_base)?.tree()?;
} else { } else {
for branch in virtual_branches.iter_mut() { for branch in virtual_branches.iter_mut() {
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 branch_tree = repo.find_commit(branch.head)?;
let branch_tree = repo.find_real_tree(&branch_tree, Default::default())?;
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)?;
if !index.has_conflicts() { if !index.has_conflicts() {

View File

@ -9,6 +9,7 @@ use gitbutler_command_context::CommandContext;
use gitbutler_diff::{diff_files_into_hunks, GitHunk, Hunk, HunkHash}; use gitbutler_diff::{diff_files_into_hunks, GitHunk, Hunk, HunkHash};
use gitbutler_operating_modes::assure_open_workspace_mode; use gitbutler_operating_modes::assure_open_workspace_mode;
use gitbutler_project::access::WorktreeWritePermission; use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::RepositoryExt;
use crate::{ use crate::{
conflicts::RepoConflictsExt, conflicts::RepoConflictsExt,
@ -33,8 +34,7 @@ pub fn get_applied_status(
ctx: &CommandContext, ctx: &CommandContext,
perm: Option<&mut WorktreeWritePermission>, perm: Option<&mut WorktreeWritePermission>,
) -> Result<VirtualBranchesStatus> { ) -> Result<VirtualBranchesStatus> {
assure_open_workspace_mode(ctx) assure_open_workspace_mode(ctx).context("ng applied status requires open workspace mode")?;
.context("Getting applied status requires open workspace mode")?;
let integration_commit = get_workspace_head(ctx)?; let integration_commit = get_workspace_head(ctx)?;
let mut virtual_branches = ctx let mut virtual_branches = ctx
.project() .project()
@ -240,7 +240,9 @@ fn compute_locks(
.iter() .iter()
.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 = repository
.find_real_tree(&commit, Default::default())
.ok()?;
let diff = repository let diff = repository
.diff_tree_to_tree(Some(&base_tree), Some(&tree), Some(opts)) .diff_tree_to_tree(Some(&base_tree), Some(&tree), Some(opts))
.ok()?; .ok()?;

View File

@ -9,8 +9,8 @@ use anyhow::{anyhow, bail, Context, Result};
use bstr::ByteSlice; use bstr::ByteSlice;
use git2_hooks::HookResult; use git2_hooks::HookResult;
use gitbutler_branch::{ use gitbutler_branch::{
dedup, dedup_fmt, reconcile_claims, Branch, BranchId, BranchOwnershipClaims, dedup, dedup_fmt, reconcile_claims, signature, Branch, BranchId, BranchOwnershipClaims,
BranchUpdateRequest, OwnershipClaim, Target, VirtualBranchesHandle, BranchUpdateRequest, OwnershipClaim, SignaturePurpose, Target, VirtualBranchesHandle,
}; };
use gitbutler_command_context::CommandContext; use gitbutler_command_context::CommandContext;
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders}; use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders};
@ -1550,6 +1550,32 @@ pub(crate) fn reorder_commit(
let parent = commit.parent(0).context("failed to find parent")?; let parent = commit.parent(0).context("failed to find parent")?;
let parent_oid = parent.id(); let parent_oid = parent.id();
let repository = ctx.repository();
let tree = repository
.find_tree(branch.tree)
.context("Failed to get branch tree")?;
let author_signature =
signature(SignaturePurpose::Author).context("Failed to get gitbutler signature")?;
let committer_signature =
signature(SignaturePurpose::Committer).context("Failed to get gitbutler signature")?;
let head = repository
.find_commit(branch.head)
.context("Failed to find branch head commit")?;
let tree_commit = repository
.commit(
None,
&author_signature,
&committer_signature,
"Branch commited changes",
&tree,
&[&head],
)
.context("Failed to commit uncommited changes")?;
if offset < 0 { if offset < 0 {
// move commit up // move commit up
if branch.head == commit_oid { if branch.head == commit_oid {
@ -1567,12 +1593,8 @@ pub(crate) fn reorder_commit(
let new_head = let new_head =
cherry_rebase_group(ctx, parent_oid, &mut ids_to_rebase).context("rebase failed")?; cherry_rebase_group(ctx, parent_oid, &mut ids_to_rebase).context("rebase failed")?;
branch.head = new_head;
branch.updated_timestamp_ms = gitbutler_time::time::now_ms();
vb_state.set_branch(branch.clone())?;
crate::integration::update_gitbutler_integration(&vb_state, ctx) branch.head = new_head;
.context("failed to update gitbutler integration")?;
} else { } else {
// move commit down // move commit down
if default_target.sha == parent_oid { if default_target.sha == parent_oid {
@ -1602,12 +1624,29 @@ pub(crate) fn reorder_commit(
cherry_rebase_group(ctx, target_oid, &mut ids_to_rebase).context("rebase failed")?; cherry_rebase_group(ctx, target_oid, &mut ids_to_rebase).context("rebase failed")?;
branch.head = new_head; branch.head = new_head;
}
let new_tree_commit =
cherry_rebase_group(ctx, branch.head, &mut [tree_commit]).context("rebase failed")?;
let new_tree_commit = repository
.find_commit(new_tree_commit)
.context("Failed to find new tree commit")?;
branch.tree = repository
.find_real_tree(&new_tree_commit, Default::default())?
.id();
// Use the conflicted tree commit as the head.
if new_tree_commit.is_conflicted() {
branch.head = new_tree_commit.id();
}
branch.updated_timestamp_ms = gitbutler_time::time::now_ms(); branch.updated_timestamp_ms = gitbutler_time::time::now_ms();
vb_state.set_branch(branch.clone())?; vb_state.set_branch(branch.clone())?;
crate::integration::update_gitbutler_integration(&vb_state, ctx) crate::integration::update_gitbutler_integration(&vb_state, ctx)
.context("failed to update gitbutler integration")?; .context("failed to update gitbutler integration")?;
}
Ok(()) Ok(())
} }
@ -1634,7 +1673,11 @@ pub(crate) fn insert_blank_commit(
commit = commit.parent(0).context("failed to find parent")?; commit = commit.parent(0).context("failed to find parent")?;
} }
let commit_tree = commit.tree().unwrap(); let repository = ctx.repository();
let commit_tree = repository
.find_real_tree(&commit, Default::default())
.unwrap();
let blank_commit_oid = ctx.commit("", &commit_tree, &[&commit], None)?; let blank_commit_oid = ctx.commit("", &commit_tree, &[&commit], None)?;
if commit.id() == branch.head && offset < 0 { if commit.id() == branch.head && offset < 0 {
@ -1677,6 +1720,10 @@ pub(crate) fn undo_commit(
.find_commit(commit_oid) .find_commit(commit_oid)
.context("failed to find commit")?; .context("failed to find commit")?;
if commit.is_conflicted() {
bail!("Can not undo a conflicted commit");
}
let new_commit_oid; let new_commit_oid;
if branch.head == commit_oid { if branch.head == commit_oid {
@ -1735,6 +1782,10 @@ pub(crate) fn squash(
.parent(0) .parent(0)
.context("failed to find parent commit")?; .context("failed to find parent commit")?;
if commit_to_squash.is_conflicted() || parent_commit.is_conflicted() {
bail!("Can not squash conflicted commits");
}
let pushed_commit_oids = branch.upstream_head.map_or_else( let pushed_commit_oids = branch.upstream_head.map_or_else(
|| Ok(vec![]), || Ok(vec![]),
|upstream_head| ctx.l(upstream_head, LogUntil::Commit(default_target.sha)), |upstream_head| ctx.l(upstream_head, LogUntil::Commit(default_target.sha)),
@ -1900,6 +1951,11 @@ pub(crate) fn move_commit(
.repository() .repository()
.find_commit(commit_id) .find_commit(commit_id)
.context("failed to find commit")?; .context("failed to find commit")?;
if source_branch_head.is_conflicted() {
bail!("Can not move conflicted commits");
}
let source_branch_head_parent = source_branch_head let source_branch_head_parent = source_branch_head
.parent(0) .parent(0)
.context("failed to get parent commit")?; .context("failed to get parent commit")?;

View File

@ -742,6 +742,7 @@ 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(CommitHeadersV2 { Some(CommitHeadersV2 {
change_id: "my-change-id".to_string(), change_id: "my-change-id".to_string(),
conflicted: None,
}), }),
) )
.expect("failed to commit"); .expect("failed to commit");

View File

@ -10,6 +10,7 @@ pub trait CommitExt {
fn message_bstr(&self) -> &BStr; fn message_bstr(&self) -> &BStr;
fn change_id(&self) -> Option<String>; fn change_id(&self) -> Option<String>;
fn is_signed(&self) -> bool; fn is_signed(&self) -> bool;
fn is_conflicted(&self) -> bool;
} }
impl<'repo> CommitExt for git2::Commit<'repo> { impl<'repo> CommitExt for git2::Commit<'repo> {
@ -23,4 +24,10 @@ impl<'repo> CommitExt for git2::Commit<'repo> {
fn is_signed(&self) -> bool { fn is_signed(&self) -> bool {
self.header_field_bytes("gpgsig").is_ok() self.header_field_bytes("gpgsig").is_ok()
} }
fn is_conflicted(&self) -> bool {
self.gitbutler_headers()
.and_then(|headers| headers.conflicted.map(|conflicted| conflicted > 0))
.unwrap_or(false)
}
} }

View File

@ -11,6 +11,9 @@ const V1_CHANGE_ID_HEADER: &str = "change-id";
/// Used to represent the old commit headers layout. This should not be used in new code /// Used to represent the old commit headers layout. This should not be used in new code
#[derive(Debug)] #[derive(Debug)]
struct CommitHeadersV1 { struct CommitHeadersV1 {
/// A property we can use to determine if two different commits are
/// actually the same "patch" at different points in time. We carry it
/// forwards when you rebase a commit in GitButler.
change_id: String, change_id: String,
} }
@ -18,19 +21,25 @@ 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_CONFLICTED_HEADER: &str = "gitbutler-conflicted";
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CommitHeadersV2 { pub struct CommitHeadersV2 {
/// A property we can use to determine if two different commits are
/// actually the same "patch" at different points in time. We carry it
/// forwards when you rebase a commit in GitButler.
pub change_id: String, pub change_id: String,
/// A property used to indicate that we've written a conflicted tree to a
/// commit. This is only written if the property is present. Conflicted
/// commits should never make it into the main trunk.
pub conflicted: Option<u64>,
} }
impl Default for CommitHeadersV2 { impl Default for CommitHeadersV2 {
fn default() -> Self { fn default() -> Self {
CommitHeadersV2 { CommitHeadersV2 {
// Change ID using base16 encoding // Change ID using base16 encoding
// NOTE(ST): Ideally, this could be a computed hash based on the patch applied, similar
// to what would happen during a rebase (if that is even the intention).
// That way, they would be stable, so tests could have reproducible hashes as well.
change_id: Uuid::new_v4().to_string(), change_id: Uuid::new_v4().to_string(),
conflicted: None,
} }
} }
} }
@ -39,6 +48,7 @@ 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,
conflicted: None,
} }
} }
} }
@ -58,7 +68,19 @@ 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();
Some(CommitHeadersV2 { change_id }) let conflicted = match self.header_field_bytes(V2_CONFLICTED_HEADER) {
Ok(value) => {
let value = dbg!(value.as_str())?;
value.parse::<u64>().ok()
}
Err(_) => None,
};
Some(CommitHeadersV2 {
change_id,
conflicted,
})
} else { } else {
// Must be for a version we don't recognise // Must be for a version we don't recognise
None None
@ -92,5 +114,9 @@ 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);
if let Some(conflicted) = self.conflicted {
commit_buffer.set_header(V2_CONFLICTED_HEADER, &conflicted.to_string())
}
} }
} }

View File

@ -6,7 +6,10 @@ use git2::build::CheckoutBuilder;
use gitbutler_branch::{signature, Branch, SignaturePurpose, VirtualBranchesHandle}; use gitbutler_branch::{signature, Branch, SignaturePurpose, VirtualBranchesHandle};
use gitbutler_branch_actions::{list_virtual_branches, update_gitbutler_integration}; use gitbutler_branch_actions::{list_virtual_branches, update_gitbutler_integration};
use gitbutler_command_context::CommandContext; use gitbutler_command_context::CommandContext;
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders}; use gitbutler_commit::{
commit_ext::CommitExt,
commit_headers::{CommitHeadersV2, HasCommitHeaders},
};
use gitbutler_operating_modes::{ use gitbutler_operating_modes::{
read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata, EDIT_BRANCH_REF, read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata, EDIT_BRANCH_REF,
INTEGRATION_BRANCH_REF, INTEGRATION_BRANCH_REF,
@ -78,14 +81,53 @@ fn checkout_edit_branch(ctx: &CommandContext, commit: &git2::Commit) -> Result<(
.checkout_head(Some(CheckoutBuilder::new().force().remove_untracked(true))) .checkout_head(Some(CheckoutBuilder::new().force().remove_untracked(true)))
.context("Failed to checkout head")?; .context("Failed to checkout head")?;
// Checkout the commit as unstaged changes
let commit_tree = commit.tree().context("Failed to get commit's tree")?; let commit_tree = commit.tree().context("Failed to get commit's tree")?;
// Checkout the commit as unstaged changes
if commit.is_conflicted() {
let base = commit_tree
.get_name(".conflict-base-0")
.context("Failed to get base")?;
let base = repository
.find_tree(base.id())
.context("Failed to find base tree")?;
// Ours
let ours = commit_tree
.get_name(".conflict-side-0")
.context("Failed to get base")?;
let ours = repository
.find_tree(ours.id())
.context("Failed to find base tree")?;
// Theirs
let theirs = commit_tree
.get_name(".conflict-side-1")
.context("Failed to get base")?;
let theirs = repository
.find_tree(theirs.id())
.context("Failed to find base tree")?;
let mut index = repository
.merge_trees(&base, &ours, &theirs, None)
.context("Failed to merge trees")?;
repository
.checkout_index(
Some(&mut index),
Some(
CheckoutBuilder::new()
.force()
.remove_untracked(true)
.conflict_style_diff3(true),
),
)
.context("Failed to checkout conflicted commit")?;
} else {
repository repository
.checkout_tree( .checkout_tree(
commit_tree.as_object(), commit_tree.as_object(),
Some(CheckoutBuilder::new().force().remove_untracked(true)), Some(CheckoutBuilder::new().force().remove_untracked(true)),
) )
.context("Failed to checkout commit")?; .context("Failed to checkout commit")?;
};
Ok(()) Ok(())
} }
@ -176,6 +218,12 @@ pub(crate) fn save_and_return_to_workspace(
let tree = repository let tree = repository
.find_tree(tree_oid) .find_tree(tree_oid)
.context("Failed to find tree")?; .context("Failed to find tree")?;
let commit_headers = commit
.gitbutler_headers()
.map(|commit_headers| CommitHeadersV2 {
conflicted: None,
..commit_headers
});
let new_commit_oid = ctx let new_commit_oid = ctx
.repository() .repository()
.commit_with_signature( .commit_with_signature(
@ -185,7 +233,7 @@ pub(crate) fn save_and_return_to_workspace(
&commit.message_bstr().to_str_lossy(), &commit.message_bstr().to_str_lossy(),
&tree, &tree,
&[&commit_parent], &[&commit_parent],
commit.gitbutler_headers(), commit_headers,
) )
.context("Failed to commit new commit")?; .context("Failed to commit new commit")?;
@ -231,8 +279,8 @@ pub(crate) fn save_and_return_to_workspace(
.find_commit(rebased_stashed_integration_changes_commit) .find_commit(rebased_stashed_integration_changes_commit)
.context("Failed to find commit of rebased stashed integration changes commit oid")?; .context("Failed to find commit of rebased stashed integration changes commit oid")?;
let tree_thing = commit_thing let tree_thing = repository
.tree() .find_real_tree(&commit_thing, Default::default())
.context("Failed to get tree of commit of rebased stashed integration changes")?; .context("Failed to get tree of commit of rebased stashed integration changes")?;
repository repository

View File

@ -96,6 +96,12 @@ pub struct Project {
pub snapshot_lines_threshold: Option<usize>, pub snapshot_lines_threshold: Option<usize>,
#[serde(default)] #[serde(default)]
pub ignore_project_semaphore: bool, pub ignore_project_semaphore: bool,
#[serde(default = "default_false")]
pub succeeding_rebases: bool,
}
fn default_false() -> bool {
false
} }
impl Project { impl Project {

View File

@ -27,6 +27,7 @@ pub struct UpdateRequest {
pub use_diff_context: Option<bool>, pub use_diff_context: Option<bool>,
pub snapshot_lines_threshold: Option<usize>, pub snapshot_lines_threshold: Option<usize>,
pub ignore_project_semaphore: Option<bool>, pub ignore_project_semaphore: Option<bool>,
pub succeeding_rebases: Option<bool>,
} }
impl Storage { impl Storage {
@ -123,6 +124,10 @@ impl Storage {
project.ignore_project_semaphore = ignore_project_semaphore; project.ignore_project_semaphore = ignore_project_semaphore;
} }
if let Some(succeeding_rebases) = update_request.succeeding_rebases {
project.succeeding_rebases = succeeding_rebases;
}
self.inner self.inner
.write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?; .write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?;

View File

@ -10,7 +10,12 @@ git2.workspace = true
gix.workspace = true gix.workspace = true
anyhow = "1.0.86" anyhow = "1.0.86"
bstr.workspace = true bstr.workspace = true
tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros", "sync" ] } tokio = { workspace = true, features = [
"rt-multi-thread",
"rt",
"macros",
"sync",
] }
gitbutler-git.workspace = true gitbutler-git.workspace = true
tracing.workspace = true tracing.workspace = true
tempfile = "3.10" tempfile = "3.10"
@ -29,6 +34,7 @@ gitbutler-id.workspace = true
gitbutler-time.workspace = true gitbutler-time.workspace = true
gitbutler-commit.workspace = true gitbutler-commit.workspace = true
gitbutler-url.workspace = true gitbutler-url.workspace = true
uuid.workspace = true
[[test]] [[test]]
name = "repo" name = "repo"

View File

@ -0,0 +1,36 @@
// tree_writer.insert(".conflict-side-0", side0.id(), 0o040000)?;
// tree_writer.insert(".conflict-side-1", side1.id(), 0o040000)?;
// tree_writer.insert(".conflict-base-0", base_tree.id(), 0o040000)?;
// tree_writer.insert(".auto-resolution", resolved_tree_id, 0o040000)?;
// tree_writer.insert(".conflict-files", conflicted_files_blob, 0o100644)?;
use std::ops::Deref;
#[derive(Default)]
pub enum ConflictedTreeKey {
/// The commit we're rebasing onto "head"
Ours,
/// The commit we're rebasing "to rebase"
Theirs,
/// The parent of "to rebase"
Base,
/// An automatic resolution of conflicts
#[default]
AutoResolution,
/// A list of conflicted files
ConflictFiles,
}
impl Deref for ConflictedTreeKey {
type Target = str;
fn deref(&self) -> &Self::Target {
match self {
ConflictedTreeKey::Ours => ".conflict-side-0",
ConflictedTreeKey::Theirs => ".conflict-side-1",
ConflictedTreeKey::Base => ".conflict-base-0",
ConflictedTreeKey::AutoResolution => ".auto-resolution",
ConflictedTreeKey::ConflictFiles => ".conflict-files",
}
}
}

View File

@ -16,3 +16,5 @@ mod config;
pub use config::Config; pub use config::Config;
pub mod askpass; pub mod askpass;
mod conflicts;

View File

@ -1,10 +1,16 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use bstr::ByteSlice; use bstr::ByteSlice;
use git2::{build::TreeUpdateBuilder, Repository};
use gitbutler_command_context::CommandContext; use gitbutler_command_context::CommandContext;
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders}; use gitbutler_commit::{
commit_ext::CommitExt,
commit_headers::{CommitHeadersV2, HasCommitHeaders},
};
use gitbutler_error::error::Marker; use gitbutler_error::error::Marker;
use tempfile::tempdir;
use uuid::Uuid;
use crate::{LogUntil, RepoActionsExt, RepositoryExt}; use crate::{conflicts::ConflictedTreeKey, LogUntil, RepoActionsExt, RepositoryExt};
/// cherry-pick based rebase, which handles empty commits /// cherry-pick based rebase, which handles empty commits
/// this function takes a commit range and generates a Vector of commit oids /// this function takes a commit range and generates a Vector of commit oids
@ -20,8 +26,6 @@ pub fn cherry_rebase(
// get a list of the commits to rebase // get a list of the commits to rebase
let mut ids_to_rebase = ctx.l(from_commit_oid, LogUntil::Commit(to_commit_oid))?; let mut ids_to_rebase = ctx.l(from_commit_oid, LogUntil::Commit(to_commit_oid))?;
dbg!(&ids_to_rebase);
if ids_to_rebase.is_empty() { if ids_to_rebase.is_empty() {
return Ok(None); return Ok(None);
} }
@ -48,37 +52,66 @@ pub fn cherry_rebase_group(
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.context("failed to read commits to rebase")?; .context("failed to read commits to rebase")?;
let repository = ctx.repository();
let new_head_id = commits_to_rebase let new_head_id = commits_to_rebase
.into_iter() .into_iter()
.fold( .fold(
ctx.repository() repository
.find_commit(target_commit_oid) .find_commit(target_commit_oid)
.context("failed to find new commit"), .context("failed to find new commit"),
|head, to_rebase| { |head, to_rebase| {
let head = head?; let head = head?;
let mut cherrypick_index = ctx let cherrypick_index = repository
.repository() .cherry_pick_gitbutler(&head, &to_rebase)
.cherrypick_commit(&to_rebase, &head, 0, None)
.context("failed to cherry pick")?; .context("failed to cherry pick")?;
if cherrypick_index.has_conflicts() { if cherrypick_index.has_conflicts() {
if !ctx.project().succeeding_rebases {
return Err(anyhow!("failed to rebase")).context(Marker::BranchConflict); return Err(anyhow!("failed to rebase")).context(Marker::BranchConflict);
} }
commit_conflicted_cherry_result(ctx, head, to_rebase, cherrypick_index)
} else {
commit_unconflicted_cherry_result(ctx, head, to_rebase, cherrypick_index)
}
},
)?
.id();
Ok(new_head_id)
}
fn commit_unconflicted_cherry_result<'repository>(
ctx: &'repository CommandContext,
head: git2::Commit<'repository>,
to_rebase: git2::Commit,
mut cherrypick_index: git2::Index,
) -> Result<git2::Commit<'repository>> {
let repository = ctx.repository();
let commit_headers = to_rebase.gitbutler_headers();
let is_merge_commit = to_rebase.parent_count() > 0;
let merge_tree_oid = cherrypick_index let merge_tree_oid = cherrypick_index
.write_tree_to(ctx.repository()) .write_tree_to(repository)
.context("failed to write merge tree")?; .context("failed to write merge tree")?;
let merge_tree = ctx // Remove empty merge commits
.repository() if is_merge_commit && merge_tree_oid == head.tree_id() {
return Ok(head);
}
let merge_tree = repository
.find_tree(merge_tree_oid) .find_tree(merge_tree_oid)
.context("failed to find merge tree")?; .context("failed to find merge tree")?;
let commit_headers = to_rebase.gitbutler_headers(); let commit_headers = commit_headers.map(|commit_headers| CommitHeadersV2 {
conflicted: None,
..commit_headers
});
let commit_oid = ctx let commit_oid = repository
.repository()
.commit_with_signature( .commit_with_signature(
None, None,
&to_rebase.author(), &to_rebase.author(),
@ -90,12 +123,153 @@ pub fn cherry_rebase_group(
) )
.context("failed to create commit")?; .context("failed to create commit")?;
ctx.repository() repository
.find_commit(commit_oid)
.context("failed to find commit")
}
fn commit_conflicted_cherry_result<'repository>(
ctx: &'repository CommandContext,
head: git2::Commit,
to_rebase: git2::Commit,
mut cherrypick_index: git2::Index,
) -> Result<git2::Commit<'repository>> {
let repository = ctx.repository();
let commit_headers = to_rebase.gitbutler_headers();
// If the commit we're rebasing is conflicted, use the commits original base.
let base_tree = if to_rebase.is_conflicted() {
repository.find_real_tree(&to_rebase, ConflictedTreeKey::Ours)?
} else {
let base_commit = to_rebase.parent(0)?;
repository.find_real_tree(&base_commit, Default::default())?
};
// in case someone checks this out with vanilla Git, we should warn why it looks like this
let readme_content =
b"You have checked out a GitButler Conflicted commit. You probably didn't mean to do this.";
let readme_blob = repository.blob(readme_content)?;
// This is what can only be described as "a tad gross" but
// AFAIK there is no good way of resolving conflicts in
// an index without writing it *somewhere*
let temporary_directory = tempdir().context("Failed to create temporary directory")?;
let branch_name = Uuid::new_v4().to_string();
let worktree = repository
.worktree(
&branch_name,
&temporary_directory.path().join("repository"),
None,
)
.context("Failed to create worktree")?;
let worktree_repository =
Repository::open_from_worktree(&worktree).context("Failed to open worktree repository")?;
worktree_repository
.set_index(&mut cherrypick_index)
.context("Failed to set cherrypick index as worktree index")?;
let mut conflicted_files = Vec::new();
// get a list of conflicted files from the index
let index_conflicts = cherrypick_index.conflicts()?.flatten().collect::<Vec<_>>();
let mut theirs: Vec<git2::IndexEntry> = vec![];
for conflict in index_conflicts {
if let Some(their) = conflict.their {
let path = std::str::from_utf8(&their.path).unwrap().to_string();
conflicted_files.push(path);
let data = repository.find_blob(their.id)?;
let data = data.content();
// For some reason we need to resolve the
// conflicts using the "their" side and
// then modify the tree afterwards.
cherrypick_index
.add_frombuffer(&their, data)
.context("Failed to add resolution")?;
theirs.push(their)
}
}
let resolved_tree = cherrypick_index
.write_tree_to(repository)
.context("Failed to write cherry index")?;
let resolved_tree = repository.find_tree(resolved_tree)?;
let mut resolved_tree_updater = TreeUpdateBuilder::new();
for their in theirs {
resolved_tree_updater.upsert(their.path, their.id, git2::FileMode::Blob);
}
let resolved_tree_id = resolved_tree_updater.create_updated(repository, &resolved_tree)?;
// convert files into a string and save as a blob
let conflicted_files_string = conflicted_files.join("\n");
let conflicted_files_blob = repository.blob(conflicted_files_string.as_bytes())?;
// create a treewriter
let mut tree_writer = repository.treebuilder(None)?;
let side0 = repository.find_real_tree(&head, ConflictedTreeKey::Ours)?;
let side1 = repository.find_real_tree(&to_rebase, ConflictedTreeKey::Theirs)?;
// save the state of the conflict, so we can recreate it later
tree_writer.insert(&*ConflictedTreeKey::Ours, side0.id(), 0o040000)?;
tree_writer.insert(&*ConflictedTreeKey::Theirs, side1.id(), 0o040000)?;
tree_writer.insert(&*ConflictedTreeKey::Base, base_tree.id(), 0o040000)?;
tree_writer.insert(
&*ConflictedTreeKey::AutoResolution,
resolved_tree_id,
0o040000,
)?;
tree_writer.insert(
&*ConflictedTreeKey::ConflictFiles,
conflicted_files_blob,
0o100644,
)?;
tree_writer.insert("README.txt", readme_blob, 0o100644)?;
let tree_oid = tree_writer.write().context("failed to write tree")?;
let commit_headers = commit_headers.map(|commit_headers| {
let conflicted_file_count = conflicted_files
.len()
.try_into()
.expect("If you have more than 2^64 conflicting files, we've got bigger problems");
CommitHeadersV2 {
conflicted: Some(conflicted_file_count),
..commit_headers
}
});
// write a commit
let commit_oid = repository
.commit_with_signature(
None,
&to_rebase.author(),
&to_rebase.committer(),
&to_rebase.message_bstr().to_str_lossy(),
&repository
.find_tree(tree_oid)
.context("failed to find tree")?,
&[&head],
commit_headers,
)
.context("failed to create commit")?;
// Tidy up worktree
{
temporary_directory.close()?;
worktree.prune(None)?;
repository
.find_branch(&branch_name, git2::BranchType::Local)?
.delete()?;
}
repository
.find_commit(commit_oid) .find_commit(commit_oid)
.context("failed to find commit") .context("failed to find commit")
},
)?
.id();
Ok(new_head_id)
} }

View File

@ -7,16 +7,30 @@ use std::{io::Write, path::Path, process::Stdio, str};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use bstr::BString; use bstr::BString;
use git2::{BlameOptions, Tree}; use git2::{BlameOptions, Tree};
use gitbutler_commit::{commit_buffer::CommitBuffer, commit_headers::CommitHeadersV2}; use gitbutler_commit::{
commit_buffer::CommitBuffer, commit_ext::CommitExt, commit_headers::CommitHeadersV2,
};
use gitbutler_config::git::{GbConfig, GitConfig}; use gitbutler_config::git::{GbConfig, GitConfig};
use gitbutler_error::error::Code; use gitbutler_error::error::Code;
use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_reference::{Refname, RemoteRefname};
use tracing::instrument; use tracing::instrument;
use crate::conflicts::ConflictedTreeKey;
/// Extension trait for `git2::Repository`. /// Extension trait for `git2::Repository`.
/// ///
/// For now, it collects useful methods from `gitbutler-core::git::Repository` /// For now, it collects useful methods from `gitbutler-core::git::Repository`
pub trait RepositoryExt { pub trait RepositoryExt {
fn cherry_pick_gitbutler(
&self,
head: &git2::Commit,
to_rebase: &git2::Commit,
) -> Result<git2::Index, anyhow::Error>;
fn find_real_tree(
&self,
commit: &git2::Commit,
side: ConflictedTreeKey,
) -> Result<git2::Tree, anyhow::Error>;
fn remote_branches(&self) -> Result<Vec<RemoteRefname>>; fn remote_branches(&self) -> Result<Vec<RemoteRefname>>;
fn remotes_as_string(&self) -> Result<Vec<String>>; fn remotes_as_string(&self) -> Result<Vec<String>>;
/// Open a new in-memory repository and executes the provided closure using it. /// Open a new in-memory repository and executes the provided closure using it.
@ -370,6 +384,54 @@ impl RepositoryExt for git2::Repository {
}) })
.collect::<Result<Vec<_>>>() .collect::<Result<Vec<_>>>()
} }
/// cherry-pick, but understands GitButler conflicted states
///
/// cherry_pick_gitbutler should always be used in favour of libgit2 or gitoxide
/// cherry pick functions
fn cherry_pick_gitbutler(
&self,
head: &git2::Commit,
to_rebase: &git2::Commit,
) -> Result<git2::Index, anyhow::Error> {
// we need to do a manual 3-way patch merge
// find the base, which is the parent of to_rebase
let base = if to_rebase.is_conflicted() {
self.find_real_tree(to_rebase, ConflictedTreeKey::Ours)?
} else {
let base_commit = to_rebase.parent(0)?;
self.find_real_tree(&base_commit, Default::default())?
};
// Get the original ours
let ours = self.find_real_tree(head, ConflictedTreeKey::Theirs)?;
// Get the original theirs
let thiers = self.find_real_tree(to_rebase, ConflictedTreeKey::Theirs)?;
self.merge_trees(&base, &ours, &thiers, None)
.context("failed to merge trees for cherry pick")
}
/// Find the real tree of a commit, which is the tree of the commit if it's not in a conflicted state
/// or the parent parent tree if it is in a conflicted state
///
/// Unless you want to find a particular side, you likly want to pass Default::default()
/// as the ConfclitedTreeKey which will give the automatically resolved resolution
fn find_real_tree(
&self,
commit: &git2::Commit,
side: ConflictedTreeKey,
) -> Result<git2::Tree, anyhow::Error> {
let tree = commit.tree()?;
if commit.is_conflicted() {
let conflicted_side = tree
.get_name(&side)
.context("Failed to get conflicted side of commit")?;
self.find_tree(conflicted_side.id())
.context("failed to find subtree")
} else {
self.find_tree(tree.id()).context("failed to find subtree")
}
}
} }
/// Signs the buffer with the configured gpg key, returning the signature. /// Signs the buffer with the configured gpg key, returning the signature.