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",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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!(
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
|
@ -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() {
|
||||||
|
@ -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()?;
|
||||||
|
@ -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")?;
|
||||||
|
@ -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");
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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)?)?;
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
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 use config::Config;
|
||||||
|
|
||||||
pub mod askpass;
|
pub mod askpass;
|
||||||
|
|
||||||
|
mod conflicts;
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user