mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-23 09:33:01 +03:00
Merge pull request #4717 from gitbutlerapp/Rebase-revolution
Rebase revolution
This commit is contained in:
commit
ec7a38f538
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2422,6 +2422,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -29,6 +29,14 @@ export class Project {
|
||||
use_new_locking!: 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.
|
||||
is_open!: boolean;
|
||||
|
||||
|
@ -10,7 +10,12 @@ class BranchDragActions {
|
||||
) {}
|
||||
|
||||
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) {
|
||||
|
@ -4,7 +4,6 @@
|
||||
import { BaseBranch } from '$lib/baseBranch/baseBranch';
|
||||
import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte';
|
||||
import { persistedCommitMessage } from '$lib/config/config';
|
||||
import { featureEditMode } from '$lib/config/uiFeatureFlags';
|
||||
import { draggableCommit } from '$lib/dragging/draggable';
|
||||
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
||||
import BranchFilesList from '$lib/file/BranchFilesList.svelte';
|
||||
@ -44,8 +43,6 @@
|
||||
const project = getContext(Project);
|
||||
const modeService = maybeGetContext(ModeService);
|
||||
|
||||
const editModeEnabled = featureEditMode();
|
||||
|
||||
const commitStore = createCommitStore(commit);
|
||||
$: commitStore.set(commit);
|
||||
|
||||
@ -135,6 +132,8 @@
|
||||
|
||||
modeService!.enterEditMode(commit.id, branch!.refname);
|
||||
}
|
||||
|
||||
$: conflicted = commit instanceof DetailedCommit && commit.conflicted;
|
||||
</script>
|
||||
|
||||
<Modal bind:this={commitMessageModal} width="small">
|
||||
@ -270,6 +269,21 @@
|
||||
<span class="commit__subtitle-divider">•</span>
|
||||
{/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
|
||||
class="commit__subtitle-btn commit__subtitle-btn_dashed"
|
||||
on:click|stopPropagation={() => copyToClipboard(commit.id)}
|
||||
@ -318,17 +332,19 @@
|
||||
{#if isUndoable}
|
||||
<div class="commit__actions hide-native-scrollbar">
|
||||
{#if isUndoable}
|
||||
<Button
|
||||
size="tag"
|
||||
style="ghost"
|
||||
outline
|
||||
icon="undo-small"
|
||||
onclick={(e) => {
|
||||
currentCommitMessage.set(commit.description);
|
||||
e.stopPropagation();
|
||||
undoCommit(commit);
|
||||
}}>Undo</Button
|
||||
>
|
||||
{#if !conflicted}
|
||||
<Button
|
||||
size="tag"
|
||||
style="ghost"
|
||||
outline
|
||||
icon="undo-small"
|
||||
onclick={(e) => {
|
||||
currentCommitMessage.set(commit.description);
|
||||
e.stopPropagation();
|
||||
undoCommit(commit);
|
||||
}}>Undo</Button
|
||||
>
|
||||
{/if}
|
||||
<Button
|
||||
size="tag"
|
||||
style="ghost"
|
||||
@ -337,8 +353,14 @@
|
||||
onclick={openCommitMessageModal}>Edit message</Button
|
||||
>
|
||||
{/if}
|
||||
{#if canEdit() && $editModeEnabled}
|
||||
<Button size="tag" style="ghost" outline onclick={editPatch}>Edit patch</Button>
|
||||
{#if canEdit() && project.succeedingRebases}
|
||||
<Button size="tag" style="ghost" outline onclick={editPatch}>
|
||||
{#if conflicted}
|
||||
Resolve conflicts
|
||||
{:else}
|
||||
Edit patch
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@ -394,6 +416,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.commit__conflicted {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
color: var(--clr-core-err-40);
|
||||
}
|
||||
|
||||
.accent-border-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -116,6 +116,8 @@
|
||||
if (isLast) return 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$: localCommitsConflicted = $localCommits.some((commit) => commit.conflicted);
|
||||
</script>
|
||||
|
||||
{#snippet reorderDropzone(dropzone: ReorderDropzone, yOffsetPx: number)}
|
||||
@ -225,6 +227,10 @@
|
||||
kind="solid"
|
||||
wide
|
||||
loading={isPushingCommits}
|
||||
disabled={localCommitsConflicted}
|
||||
help={localCommitsConflicted
|
||||
? 'In order to push, please resolve any conflicted commits.'
|
||||
: undefined}
|
||||
onclick={async () => {
|
||||
isPushingCommits = true;
|
||||
try {
|
||||
|
@ -34,13 +34,15 @@ export class CommitDragActions {
|
||||
if (
|
||||
data instanceof DraggableHunk &&
|
||||
data.branchId === this.branch.id &&
|
||||
data.commitId !== this.commit.id
|
||||
data.commitId !== this.commit.id &&
|
||||
!this.commit.conflicted
|
||||
) {
|
||||
return true;
|
||||
} else if (
|
||||
data instanceof DraggableFile &&
|
||||
data.branchId === this.branch.id &&
|
||||
data.commit?.id !== this.commit.id
|
||||
data.commit?.id !== this.commit.id &&
|
||||
!this.commit.conflicted
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
@ -79,6 +81,8 @@ export class CommitDragActions {
|
||||
if (!(data instanceof DraggableCommit)) 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.isIntegrated) return false;
|
||||
if (data.commit.isRemote && !this.project.ok_with_force_push) return false;
|
||||
|
@ -38,7 +38,10 @@
|
||||
</span>
|
||||
</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">
|
||||
<Button
|
||||
|
@ -15,8 +15,3 @@ export function featureInlineUnifiedDiffs(): Persisted<boolean> {
|
||||
const key = 'inlineUnifiedDiffs';
|
||||
return persisted(false, key);
|
||||
}
|
||||
|
||||
export function featureEditMode(): Persisted<boolean> {
|
||||
const key = 'editMode';
|
||||
return persisted(false, key);
|
||||
}
|
||||
|
@ -71,6 +71,12 @@
|
||||
let loading = true;
|
||||
let signCheckResult = false;
|
||||
let errorMessage = '';
|
||||
let succeedingRebases = project.succeedingRebases;
|
||||
|
||||
$: {
|
||||
project.succeedingRebases = succeedingRebases;
|
||||
projectService.updateProject(project);
|
||||
}
|
||||
|
||||
async function checkSigning() {
|
||||
checked = true;
|
||||
@ -308,4 +314,16 @@
|
||||
<Toggle id="ignoreProjectSemaphore" bind:checked={ignoreProjectSemaphore} />
|
||||
</svelte:fragment>
|
||||
</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>
|
||||
|
@ -172,6 +172,7 @@ export class DetailedCommit {
|
||||
changeId!: string;
|
||||
isSigned!: boolean;
|
||||
relatedTo?: Commit;
|
||||
conflicted!: boolean;
|
||||
|
||||
prev?: DetailedCommit;
|
||||
next?: DetailedCommit;
|
||||
|
@ -2,7 +2,6 @@
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import {
|
||||
featureBaseBranchSwitching,
|
||||
featureEditMode,
|
||||
featureInlineUnifiedDiffs
|
||||
} from '$lib/config/uiFeatureFlags';
|
||||
import ContentWrapper from '$lib/settings/ContentWrapper.svelte';
|
||||
@ -10,7 +9,6 @@
|
||||
|
||||
const baseBranchSwitching = featureBaseBranchSwitching();
|
||||
const inlineUnifiedDiffs = featureInlineUnifiedDiffs();
|
||||
const editMode = featureEditMode();
|
||||
</script>
|
||||
|
||||
<ContentWrapper title="Experimental features">
|
||||
@ -47,20 +45,6 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</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>
|
||||
|
||||
<style>
|
||||
|
@ -360,8 +360,7 @@ pub(crate) fn update_base_branch(
|
||||
let updated_vbranches = get_applied_status(ctx, None)?
|
||||
.branches
|
||||
.into_iter()
|
||||
.map(|(branch, _)| branch)
|
||||
.map(|mut branch: Branch| -> Result<Option<Branch>> {
|
||||
.map(|(mut branch, _)| -> Result<Option<Branch>> {
|
||||
let branch_tree = repo.find_tree(branch.tree)?;
|
||||
|
||||
let branch_head_commit = repo.find_commit(branch.head).context(format!(
|
||||
|
@ -33,6 +33,7 @@ pub struct VirtualBranchCommit {
|
||||
pub branch_id: BranchId,
|
||||
pub change_id: Option<String>,
|
||||
pub is_signed: bool,
|
||||
pub conflicted: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn commit_to_vbranch_commit(
|
||||
@ -68,6 +69,7 @@ pub(crate) fn commit_to_vbranch_commit(
|
||||
branch_id: branch.id,
|
||||
change_id: commit.change_id(),
|
||||
is_signed: commit.is_signed(),
|
||||
conflicted: commit.is_conflicted(),
|
||||
};
|
||||
|
||||
Ok(commit)
|
||||
|
@ -6,6 +6,7 @@ use std::{
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_diff::FileDiff;
|
||||
use gitbutler_repo::RepositoryExt;
|
||||
use serde::Serialize;
|
||||
|
||||
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 commit_tree = commit.tree().context("failed to get commit tree")?;
|
||||
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_files = gitbutler_diff::trees(repository, &parent_tree, &commit_tree)?;
|
||||
|
||||
Ok(diff_files
|
||||
@ -93,8 +98,13 @@ pub(crate) fn list_virtual_commit_files(
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let parent = commit.parent(0).context("failed to get parent commit")?;
|
||||
let commit_tree = commit.tree().context("failed to get commit tree")?;
|
||||
let parent_tree = parent.tree().context("failed to get parent tree")?;
|
||||
let repository = ctx.repository();
|
||||
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 hunks_by_filepath = virtual_hunks_by_file_diffs(&ctx.project().path, diff);
|
||||
Ok(virtual_hunks_into_virtual_files(ctx, hunks_by_filepath))
|
||||
|
@ -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 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)? {
|
||||
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()?;
|
||||
} else {
|
||||
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 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)?;
|
||||
|
||||
if !index.has_conflicts() {
|
||||
|
@ -9,6 +9,7 @@ use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_diff::{diff_files_into_hunks, GitHunk, Hunk, HunkHash};
|
||||
use gitbutler_operating_modes::assure_open_workspace_mode;
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_repo::RepositoryExt;
|
||||
|
||||
use crate::{
|
||||
conflicts::RepoConflictsExt,
|
||||
@ -33,8 +34,7 @@ pub fn get_applied_status(
|
||||
ctx: &CommandContext,
|
||||
perm: Option<&mut WorktreeWritePermission>,
|
||||
) -> Result<VirtualBranchesStatus> {
|
||||
assure_open_workspace_mode(ctx)
|
||||
.context("Getting applied status requires open workspace mode")?;
|
||||
assure_open_workspace_mode(ctx).context("ng applied status requires open workspace mode")?;
|
||||
let integration_commit = get_workspace_head(ctx)?;
|
||||
let mut virtual_branches = ctx
|
||||
.project()
|
||||
@ -240,7 +240,9 @@ fn compute_locks(
|
||||
.iter()
|
||||
.filter_map(|branch| {
|
||||
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
|
||||
.diff_tree_to_tree(Some(&base_tree), Some(&tree), Some(opts))
|
||||
.ok()?;
|
||||
|
@ -9,8 +9,8 @@ use anyhow::{anyhow, bail, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use git2_hooks::HookResult;
|
||||
use gitbutler_branch::{
|
||||
dedup, dedup_fmt, reconcile_claims, Branch, BranchId, BranchOwnershipClaims,
|
||||
BranchUpdateRequest, OwnershipClaim, Target, VirtualBranchesHandle,
|
||||
dedup, dedup_fmt, reconcile_claims, signature, Branch, BranchId, BranchOwnershipClaims,
|
||||
BranchUpdateRequest, OwnershipClaim, SignaturePurpose, Target, VirtualBranchesHandle,
|
||||
};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
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_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 {
|
||||
// move commit up
|
||||
if branch.head == commit_oid {
|
||||
@ -1567,12 +1593,8 @@ pub(crate) fn reorder_commit(
|
||||
|
||||
let new_head =
|
||||
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)
|
||||
.context("failed to update gitbutler integration")?;
|
||||
branch.head = new_head;
|
||||
} else {
|
||||
// move commit down
|
||||
if default_target.sha == parent_oid {
|
||||
@ -1602,13 +1624,30 @@ pub(crate) fn reorder_commit(
|
||||
cherry_rebase_group(ctx, target_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)
|
||||
.context("failed to update gitbutler integration")?;
|
||||
}
|
||||
|
||||
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();
|
||||
vb_state.set_branch(branch.clone())?;
|
||||
|
||||
crate::integration::update_gitbutler_integration(&vb_state, ctx)
|
||||
.context("failed to update gitbutler integration")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1634,7 +1673,11 @@ pub(crate) fn insert_blank_commit(
|
||||
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)?;
|
||||
|
||||
if commit.id() == branch.head && offset < 0 {
|
||||
@ -1677,6 +1720,10 @@ pub(crate) fn undo_commit(
|
||||
.find_commit(commit_oid)
|
||||
.context("failed to find commit")?;
|
||||
|
||||
if commit.is_conflicted() {
|
||||
bail!("Can not undo a conflicted commit");
|
||||
}
|
||||
|
||||
let new_commit_oid;
|
||||
|
||||
if branch.head == commit_oid {
|
||||
@ -1735,6 +1782,10 @@ pub(crate) fn squash(
|
||||
.parent(0)
|
||||
.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(
|
||||
|| Ok(vec![]),
|
||||
|upstream_head| ctx.l(upstream_head, LogUntil::Commit(default_target.sha)),
|
||||
@ -1900,6 +1951,11 @@ pub(crate) fn move_commit(
|
||||
.repository()
|
||||
.find_commit(commit_id)
|
||||
.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
|
||||
.parent(0)
|
||||
.context("failed to get parent commit")?;
|
||||
|
@ -742,6 +742,7 @@ fn commit_id_can_be_generated_or_specified() -> Result<()> {
|
||||
// The change ID should always be generated by calling CommitHeadersV2::new
|
||||
Some(CommitHeadersV2 {
|
||||
change_id: "my-change-id".to_string(),
|
||||
conflicted: None,
|
||||
}),
|
||||
)
|
||||
.expect("failed to commit");
|
||||
|
@ -10,6 +10,7 @@ pub trait CommitExt {
|
||||
fn message_bstr(&self) -> &BStr;
|
||||
fn change_id(&self) -> Option<String>;
|
||||
fn is_signed(&self) -> bool;
|
||||
fn is_conflicted(&self) -> bool;
|
||||
}
|
||||
|
||||
impl<'repo> CommitExt for git2::Commit<'repo> {
|
||||
@ -23,4 +24,10 @@ impl<'repo> CommitExt for git2::Commit<'repo> {
|
||||
fn is_signed(&self) -> bool {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
#[derive(Debug)]
|
||||
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,
|
||||
}
|
||||
|
||||
@ -18,19 +21,25 @@ struct CommitHeadersV1 {
|
||||
const V2_HEADERS_VERSION: &str = "2";
|
||||
|
||||
const V2_CHANGE_ID_HEADER: &str = "gitbutler-change-id";
|
||||
const V2_CONFLICTED_HEADER: &str = "gitbutler-conflicted";
|
||||
#[derive(Debug, Clone)]
|
||||
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,
|
||||
/// 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 {
|
||||
fn default() -> Self {
|
||||
CommitHeadersV2 {
|
||||
// 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(),
|
||||
conflicted: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,6 +48,7 @@ impl From<CommitHeadersV1> for CommitHeadersV2 {
|
||||
fn from(commit_headers_v1: CommitHeadersV1) -> CommitHeadersV2 {
|
||||
CommitHeadersV2 {
|
||||
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
|
||||
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 {
|
||||
// Must be for a version we don't recognise
|
||||
None
|
||||
@ -92,5 +114,9 @@ impl CommitHeadersV2 {
|
||||
pub fn inject_into(&self, commit_buffer: &mut CommitBuffer) {
|
||||
commit_buffer.set_header(HEADERS_VERSION_HEADER, V2_HEADERS_VERSION);
|
||||
commit_buffer.set_header(V2_CHANGE_ID_HEADER, &self.change_id);
|
||||
|
||||
if let Some(conflicted) = self.conflicted {
|
||||
commit_buffer.set_header(V2_CONFLICTED_HEADER, &conflicted.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,10 @@ use git2::build::CheckoutBuilder;
|
||||
use gitbutler_branch::{signature, Branch, SignaturePurpose, VirtualBranchesHandle};
|
||||
use gitbutler_branch_actions::{list_virtual_branches, update_gitbutler_integration};
|
||||
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::{
|
||||
read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata, EDIT_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)))
|
||||
.context("Failed to checkout head")?;
|
||||
|
||||
// Checkout the commit as unstaged changes
|
||||
let commit_tree = commit.tree().context("Failed to get commit's tree")?;
|
||||
repository
|
||||
.checkout_tree(
|
||||
commit_tree.as_object(),
|
||||
Some(CheckoutBuilder::new().force().remove_untracked(true)),
|
||||
)
|
||||
.context("Failed to checkout commit")?;
|
||||
// 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
|
||||
.checkout_tree(
|
||||
commit_tree.as_object(),
|
||||
Some(CheckoutBuilder::new().force().remove_untracked(true)),
|
||||
)
|
||||
.context("Failed to checkout commit")?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -176,6 +218,12 @@ pub(crate) fn save_and_return_to_workspace(
|
||||
let tree = repository
|
||||
.find_tree(tree_oid)
|
||||
.context("Failed to find tree")?;
|
||||
let commit_headers = commit
|
||||
.gitbutler_headers()
|
||||
.map(|commit_headers| CommitHeadersV2 {
|
||||
conflicted: None,
|
||||
..commit_headers
|
||||
});
|
||||
let new_commit_oid = ctx
|
||||
.repository()
|
||||
.commit_with_signature(
|
||||
@ -185,7 +233,7 @@ pub(crate) fn save_and_return_to_workspace(
|
||||
&commit.message_bstr().to_str_lossy(),
|
||||
&tree,
|
||||
&[&commit_parent],
|
||||
commit.gitbutler_headers(),
|
||||
commit_headers,
|
||||
)
|
||||
.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)
|
||||
.context("Failed to find commit of rebased stashed integration changes commit oid")?;
|
||||
|
||||
let tree_thing = commit_thing
|
||||
.tree()
|
||||
let tree_thing = repository
|
||||
.find_real_tree(&commit_thing, Default::default())
|
||||
.context("Failed to get tree of commit of rebased stashed integration changes")?;
|
||||
|
||||
repository
|
||||
|
@ -96,6 +96,12 @@ pub struct Project {
|
||||
pub snapshot_lines_threshold: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub ignore_project_semaphore: bool,
|
||||
#[serde(default = "default_false")]
|
||||
pub succeeding_rebases: bool,
|
||||
}
|
||||
|
||||
fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl Project {
|
||||
|
@ -27,6 +27,7 @@ pub struct UpdateRequest {
|
||||
pub use_diff_context: Option<bool>,
|
||||
pub snapshot_lines_threshold: Option<usize>,
|
||||
pub ignore_project_semaphore: Option<bool>,
|
||||
pub succeeding_rebases: Option<bool>,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
@ -123,6 +124,10 @@ impl Storage {
|
||||
project.ignore_project_semaphore = ignore_project_semaphore;
|
||||
}
|
||||
|
||||
if let Some(succeeding_rebases) = update_request.succeeding_rebases {
|
||||
project.succeeding_rebases = succeeding_rebases;
|
||||
}
|
||||
|
||||
self.inner
|
||||
.write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?;
|
||||
|
||||
|
@ -10,11 +10,16 @@ git2.workspace = true
|
||||
gix.workspace = true
|
||||
anyhow = "1.0.86"
|
||||
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
|
||||
tracing.workspace = true
|
||||
tempfile = "3.10"
|
||||
serde = { workspace = true, features = ["std"]}
|
||||
serde = { workspace = true, features = ["std"] }
|
||||
log = "^0.4"
|
||||
thiserror.workspace = true
|
||||
resolve-path = "0.1.0"
|
||||
@ -29,12 +34,13 @@ gitbutler-id.workspace = true
|
||||
gitbutler-time.workspace = true
|
||||
gitbutler-commit.workspace = true
|
||||
gitbutler-url.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[[test]]
|
||||
name="repo"
|
||||
name = "repo"
|
||||
path = "tests/mod.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
gitbutler-testsupport.workspace = true
|
||||
gitbutler-user.workspace = true
|
||||
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
|
||||
serde_json = { version = "1.0", features = ["std", "arbitrary_precision"] }
|
||||
|
36
crates/gitbutler-repo/src/conflicts.rs
Normal file
36
crates/gitbutler-repo/src/conflicts.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
@ -16,3 +16,5 @@ mod config;
|
||||
pub use config::Config;
|
||||
|
||||
pub mod askpass;
|
||||
|
||||
mod conflicts;
|
||||
|
@ -1,10 +1,16 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use git2::{build::TreeUpdateBuilder, Repository};
|
||||
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 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
|
||||
/// 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
|
||||
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() {
|
||||
return Ok(None);
|
||||
}
|
||||
@ -48,54 +52,224 @@ pub fn cherry_rebase_group(
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.context("failed to read commits to rebase")?;
|
||||
|
||||
let repository = ctx.repository();
|
||||
|
||||
let new_head_id = commits_to_rebase
|
||||
.into_iter()
|
||||
.fold(
|
||||
ctx.repository()
|
||||
repository
|
||||
.find_commit(target_commit_oid)
|
||||
.context("failed to find new commit"),
|
||||
|head, to_rebase| {
|
||||
let head = head?;
|
||||
|
||||
let mut cherrypick_index = ctx
|
||||
.repository()
|
||||
.cherrypick_commit(&to_rebase, &head, 0, None)
|
||||
let cherrypick_index = repository
|
||||
.cherry_pick_gitbutler(&head, &to_rebase)
|
||||
.context("failed to cherry pick")?;
|
||||
|
||||
if cherrypick_index.has_conflicts() {
|
||||
return Err(anyhow!("failed to rebase")).context(Marker::BranchConflict);
|
||||
if !ctx.project().succeeding_rebases {
|
||||
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)
|
||||
}
|
||||
|
||||
let merge_tree_oid = cherrypick_index
|
||||
.write_tree_to(ctx.repository())
|
||||
.context("failed to write merge tree")?;
|
||||
|
||||
let merge_tree = ctx
|
||||
.repository()
|
||||
.find_tree(merge_tree_oid)
|
||||
.context("failed to find merge tree")?;
|
||||
|
||||
let commit_headers = to_rebase.gitbutler_headers();
|
||||
|
||||
let commit_oid = ctx
|
||||
.repository()
|
||||
.commit_with_signature(
|
||||
None,
|
||||
&to_rebase.author(),
|
||||
&to_rebase.committer(),
|
||||
&to_rebase.message_bstr().to_str_lossy(),
|
||||
&merge_tree,
|
||||
&[&head],
|
||||
commit_headers,
|
||||
)
|
||||
.context("failed to create commit")?;
|
||||
|
||||
ctx.repository()
|
||||
.find_commit(commit_oid)
|
||||
.context("failed to find commit")
|
||||
},
|
||||
)?
|
||||
.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
|
||||
.write_tree_to(repository)
|
||||
.context("failed to write merge tree")?;
|
||||
|
||||
// Remove empty merge commits
|
||||
if is_merge_commit && merge_tree_oid == head.tree_id() {
|
||||
return Ok(head);
|
||||
}
|
||||
|
||||
let merge_tree = repository
|
||||
.find_tree(merge_tree_oid)
|
||||
.context("failed to find merge tree")?;
|
||||
|
||||
let commit_headers = commit_headers.map(|commit_headers| CommitHeadersV2 {
|
||||
conflicted: None,
|
||||
..commit_headers
|
||||
});
|
||||
|
||||
let commit_oid = repository
|
||||
.commit_with_signature(
|
||||
None,
|
||||
&to_rebase.author(),
|
||||
&to_rebase.committer(),
|
||||
&to_rebase.message_bstr().to_str_lossy(),
|
||||
&merge_tree,
|
||||
&[&head],
|
||||
commit_headers,
|
||||
)
|
||||
.context("failed to create commit")?;
|
||||
|
||||
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)
|
||||
.context("failed to find commit")
|
||||
}
|
||||
|
@ -7,16 +7,30 @@ use std::{io::Write, path::Path, process::Stdio, str};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use bstr::BString;
|
||||
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_error::error::Code;
|
||||
use gitbutler_reference::{Refname, RemoteRefname};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::conflicts::ConflictedTreeKey;
|
||||
|
||||
/// Extension trait for `git2::Repository`.
|
||||
///
|
||||
/// For now, it collects useful methods from `gitbutler-core::git::Repository`
|
||||
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 remotes_as_string(&self) -> Result<Vec<String>>;
|
||||
/// 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<_>>>()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
Loading…
Reference in New Issue
Block a user