mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-26 12:24:26 +03:00
Add edit mode actions
More edit mode
This commit is contained in:
parent
daa285f41e
commit
7ab7731a31
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -2194,6 +2194,24 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler-edit-mode"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"git2",
|
||||
"gitbutler-branch",
|
||||
"gitbutler-branch-actions",
|
||||
"gitbutler-command-context",
|
||||
"gitbutler-commit",
|
||||
"gitbutler-operating-modes",
|
||||
"gitbutler-project",
|
||||
"gitbutler-reference",
|
||||
"gitbutler-repo",
|
||||
"gitbutler-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler-error"
|
||||
version = "0.0.0"
|
||||
@ -2445,6 +2463,7 @@ dependencies = [
|
||||
"gitbutler-command-context",
|
||||
"gitbutler-config",
|
||||
"gitbutler-diff",
|
||||
"gitbutler-edit-mode",
|
||||
"gitbutler-error",
|
||||
"gitbutler-feedback",
|
||||
"gitbutler-id",
|
||||
|
@ -29,6 +29,7 @@ members = [
|
||||
"crates/gitbutler-url",
|
||||
"crates/gitbutler-diff",
|
||||
"crates/gitbutler-operating-modes",
|
||||
"crates/gitbutler-edit-mode",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@ -79,6 +80,7 @@ gitbutler-tagged-string = { path = "crates/gitbutler-tagged-string" }
|
||||
gitbutler-url = { path = "crates/gitbutler-url" }
|
||||
gitbutler-diff = { path = "crates/gitbutler-diff" }
|
||||
gitbutler-operating-modes = { path = "crates/gitbutler-operating-modes" }
|
||||
gitbutler-edit-mode = { path = "crates/gitbutler-edit-mode" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
|
@ -7,8 +7,9 @@
|
||||
import { draggableCommit } from '$lib/dragging/draggable';
|
||||
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
||||
import BranchFilesList from '$lib/file/BranchFilesList.svelte';
|
||||
import { ModeService } from '$lib/modes/service';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { getContext, getContextStore, maybeGetContext } from '$lib/utils/context';
|
||||
import { openExternalUrl } from '$lib/utils/url';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { createCommitStore } from '$lib/vbranches/contexts';
|
||||
@ -37,9 +38,12 @@
|
||||
export let type: CommitStatus;
|
||||
export let lines: Snippet<[number]> | undefined = undefined;
|
||||
|
||||
$: console.log(branch);
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const baseBranch = getContextStore(BaseBranch);
|
||||
const project = getContext(Project);
|
||||
const modeService = maybeGetContext(ModeService);
|
||||
|
||||
const commitStore = createCommitStore(commit);
|
||||
$: commitStore.set(commit);
|
||||
@ -116,6 +120,20 @@
|
||||
|
||||
let dragDirection: 'up' | 'down' | undefined;
|
||||
let isDragTargeted = false;
|
||||
|
||||
function canEdit() {
|
||||
if (isUnapplied) return false;
|
||||
if (!modeService) return false;
|
||||
if (!branch) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function edit() {
|
||||
if (!canEdit()) return;
|
||||
|
||||
modeService!.enterEditMode(commit.id, branch!.refname);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={commitMessageModal} width="small">
|
||||
@ -318,6 +336,9 @@
|
||||
onclick={openCommitMessageModal}>Edit message</Button
|
||||
>
|
||||
{/if}
|
||||
{#if canEdit()}
|
||||
<Button size="tag" style="ghost" outline onclick={edit}>Edit patch</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
78
apps/desktop/src/lib/components/EditMode.svelte
Normal file
78
apps/desktop/src/lib/components/EditMode.svelte
Normal file
@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import DecorativeSplitView from './DecorativeSplitView.svelte';
|
||||
import ProjectNameLabel from '../shared/ProjectNameLabel.svelte';
|
||||
import dzenPc from '$lib/assets/dzen-pc.svg?raw';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { ModeService, type EditModeMetadata } from '$lib/modes/service';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
|
||||
interface Props {
|
||||
editModeMetadata: EditModeMetadata;
|
||||
}
|
||||
|
||||
const { editModeMetadata }: Props = $props();
|
||||
|
||||
const project = getContext(Project);
|
||||
const modeService = getContext(ModeService);
|
||||
|
||||
let modeServiceSaving = $state<'inert' | 'loading' | 'completed'>('inert');
|
||||
|
||||
async function save() {
|
||||
modeServiceSaving = 'loading';
|
||||
|
||||
await modeService.saveEditAndReturnToWorkspace();
|
||||
|
||||
modeServiceSaving = 'completed';
|
||||
}
|
||||
</script>
|
||||
|
||||
<DecorativeSplitView img={dzenPc}>
|
||||
<div class="switchrepo">
|
||||
<div class="project-name">
|
||||
<ProjectNameLabel projectName={project?.title} />
|
||||
</div>
|
||||
<p class="switchrepo__title text-18 text-body text-bold">
|
||||
You are currently editing commit <span class="code-string">
|
||||
{editModeMetadata.editeeCommitSha.slice(0, 7)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p class="switchrepo__message text-13 text-body">Bla bla bla</p>
|
||||
|
||||
<div class="switchrepo__actions">
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
icon="undo-small"
|
||||
reversedDirection
|
||||
onclick={save}
|
||||
loading={modeServiceSaving === 'loading'}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DecorativeSplitView>
|
||||
|
||||
<style lang="postcss">
|
||||
.project-name {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.switchrepo__title {
|
||||
color: var(--clr-scale-ntrl-30);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.switchrepo__message {
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.switchrepo__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,18 @@
|
||||
import { invoke, listen } from '$lib/backend/ipc';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
type Mode = { type: 'OpenWorkspace' } | { type: 'OutsideWorksapce' } | { type: 'Edit' };
|
||||
export interface EditModeMetadata {
|
||||
editeeCommitSha: string;
|
||||
editeeBranch: string;
|
||||
}
|
||||
|
||||
type Mode =
|
||||
| { type: 'OpenWorkspace' }
|
||||
| { type: 'OutsideWorkspace' }
|
||||
| {
|
||||
type: 'Edit';
|
||||
subject: EditModeMetadata;
|
||||
};
|
||||
interface HeadAndMode {
|
||||
head?: string;
|
||||
operatingMode?: Mode;
|
||||
@ -29,6 +40,20 @@ export class ModeService {
|
||||
|
||||
this.headAndMode.set({ head, operatingMode });
|
||||
}
|
||||
|
||||
async enterEditMode(editeeCommitId: string, editeeBranchRef: string) {
|
||||
await invoke('enter_edit_mode', {
|
||||
projectId: this.projectId,
|
||||
editee: editeeCommitId,
|
||||
editeeBranch: editeeBranchRef
|
||||
});
|
||||
}
|
||||
|
||||
async saveEditAndReturnToWorkspace() {
|
||||
await invoke('save_edit_and_return_to_workspace', {
|
||||
projectId: this.projectId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToHead(projectId: string, callback: (headAndMode: HeadAndMode) => void) {
|
||||
|
@ -132,6 +132,7 @@ export class VirtualBranch {
|
||||
forkPoint!: string;
|
||||
allowRebasing!: boolean;
|
||||
pr?: PullRequest;
|
||||
refname!: string;
|
||||
|
||||
get localCommits() {
|
||||
return this.commits.filter((c) => c.status === 'local');
|
||||
|
@ -9,6 +9,7 @@
|
||||
import { getNameNormalizationServiceContext } from '$lib/branches/nameNormalizationService';
|
||||
import { BranchService, createBranchServiceStore } from '$lib/branches/service';
|
||||
import { CommitDragActionsFactory } from '$lib/commits/dragActions';
|
||||
import EditMode from '$lib/components/EditMode.svelte';
|
||||
import NoBaseBranch from '$lib/components/NoBaseBranch.svelte';
|
||||
import NotOnGitButlerBranch from '$lib/components/NotOnGitButlerBranch.svelte';
|
||||
import ProblemLoadingRepo from '$lib/components/ProblemLoadingRepo.svelte';
|
||||
@ -20,6 +21,7 @@
|
||||
import History from '$lib/history/History.svelte';
|
||||
import { HistoryService } from '$lib/history/history';
|
||||
import MetricsReporter from '$lib/metrics/MetricsReporter.svelte';
|
||||
import { ModeService } from '$lib/modes/service';
|
||||
import Navigation from '$lib/navigation/Navigation.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
@ -69,6 +71,7 @@
|
||||
setContext(ReorderDropzoneManagerFactory, data.reorderDropzoneManagerFactory);
|
||||
setContext(RemoteBranchService, data.remoteBranchService);
|
||||
setContext(BranchListingService, data.branchListingService);
|
||||
setContext(ModeService, data.modeService);
|
||||
});
|
||||
|
||||
let intervalId: any;
|
||||
@ -98,7 +101,6 @@
|
||||
// Refresh base branch if git fetch event is detected.
|
||||
const mode = $derived(modeService.mode);
|
||||
const head = $derived(modeService.head);
|
||||
const openWorkspace = $derived($mode?.type === 'OpenWorkspace');
|
||||
|
||||
// We end up with a `state_unsafe_mutation` when switching projects if we
|
||||
// don't use $effect.pre here.
|
||||
@ -160,6 +162,8 @@
|
||||
onDestroy(() => {
|
||||
clearFetchInterval();
|
||||
});
|
||||
|
||||
$inspect($mode);
|
||||
</script>
|
||||
|
||||
<!-- forces components to be recreated when projectId changes -->
|
||||
@ -180,9 +184,8 @@
|
||||
<ProblemLoadingRepo error={$branchesError} />
|
||||
{:else if $projectError}
|
||||
<ProblemLoadingRepo error={$projectError} />
|
||||
{:else if !openWorkspace && $baseBranch}
|
||||
<NotOnGitButlerBranch baseBranch={$baseBranch} />
|
||||
{:else if $baseBranch}
|
||||
{#if $mode?.type === 'OpenWorkspace'}
|
||||
<div class="view-wrap" role="group" ondragover={(e) => e.preventDefault()}>
|
||||
<Navigation />
|
||||
{#if $showHistoryView}
|
||||
@ -190,6 +193,11 @@
|
||||
{/if}
|
||||
{@render children()}
|
||||
</div>
|
||||
{:else if $mode?.type === 'OutsideWorkspace'}
|
||||
<NotOnGitButlerBranch baseBranch={$baseBranch} />
|
||||
{:else if $mode?.type === 'Edit'}
|
||||
<EditMode editModeMetadata={$mode.subject} />
|
||||
{/if}
|
||||
{/if}
|
||||
<MetricsReporter {projectMetrics} />
|
||||
{/key}
|
||||
|
@ -76,6 +76,7 @@ pub struct VirtualBranch {
|
||||
/// The fork point between the target branch and the virtual branch
|
||||
#[serde(with = "gitbutler_serde::oid_opt", default)]
|
||||
pub fork_point: Option<git2::Oid>,
|
||||
pub refname: Refname,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||
@ -366,6 +367,8 @@ pub fn list_virtual_branches(
|
||||
.and_then(|c| c.parent(0).ok())
|
||||
.map(|c| c.id());
|
||||
|
||||
let refname = branch.refname()?.into();
|
||||
|
||||
let branch = VirtualBranch {
|
||||
id: branch.id,
|
||||
name: branch.name,
|
||||
@ -388,6 +391,7 @@ pub fn list_virtual_branches(
|
||||
head: branch.head,
|
||||
merge_base,
|
||||
fork_point,
|
||||
refname,
|
||||
};
|
||||
branches.push(branch);
|
||||
}
|
||||
|
20
crates/gitbutler-edit-mode/Cargo.toml
Normal file
20
crates/gitbutler-edit-mode/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "gitbutler-edit-mode"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
authors = ["GitButler <gitbutler@gitbutler.com>"]
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
git2.workspace = true
|
||||
anyhow.workspace = true
|
||||
bstr.workspace = true
|
||||
gitbutler-branch.workspace = true
|
||||
gitbutler-commit.workspace = true
|
||||
gitbutler-repo.workspace = true
|
||||
gitbutler-command-context.workspace = true
|
||||
gitbutler-operating-modes.workspace = true
|
||||
gitbutler-project.workspace = true
|
||||
gitbutler-branch-actions.workspace = true
|
||||
gitbutler-reference.workspace = true
|
||||
gitbutler-time.workspace = true
|
42
crates/gitbutler-edit-mode/src/commands.rs
Normal file
42
crates/gitbutler-edit-mode/src/commands.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_operating_modes::{assure_edit_mode, assure_open_workspace_mode, EditModeMetadata};
|
||||
use gitbutler_project::{access::WriteWorkspaceGuard, Project};
|
||||
use gitbutler_reference::ReferenceName;
|
||||
|
||||
pub fn enter_edit_mode(
|
||||
project: &Project,
|
||||
editee: git2::Oid,
|
||||
editee_branch: ReferenceName,
|
||||
) -> Result<EditModeMetadata> {
|
||||
let (ctx, mut guard) = open_with_permission(project)?;
|
||||
|
||||
assure_open_workspace_mode(&ctx)
|
||||
.context("Entering edit mode may only be done when the workspace is open")?;
|
||||
|
||||
let editee = ctx
|
||||
.repository()
|
||||
.find_commit(editee)
|
||||
.context("Failed to find editee commit")?;
|
||||
|
||||
let editee_branch = ctx
|
||||
.repository()
|
||||
.find_reference(&editee_branch)
|
||||
.context("Failed to find editee branch reference")?;
|
||||
|
||||
crate::enter_edit_mode(&ctx, &editee, &editee_branch, guard.write_permission())
|
||||
}
|
||||
|
||||
pub fn save_and_return_to_workspace(project: &Project) -> Result<()> {
|
||||
let (ctx, mut guard) = open_with_permission(project)?;
|
||||
|
||||
assure_edit_mode(&ctx).context("Edit mode may only be left while in edit mode")?;
|
||||
|
||||
crate::save_and_return_to_workspace(&ctx, guard.write_permission())
|
||||
}
|
||||
|
||||
fn open_with_permission(project: &Project) -> Result<(CommandContext, WriteWorkspaceGuard)> {
|
||||
let ctx = CommandContext::open(project)?;
|
||||
let guard = project.exclusive_worktree_access();
|
||||
Ok((ctx, guard))
|
||||
}
|
250
crates/gitbutler-edit-mode/src/lib.rs
Normal file
250
crates/gitbutler-edit-mode/src/lib.rs
Normal file
@ -0,0 +1,250 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
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_operating_modes::{
|
||||
read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata, EDIT_BRANCH_REF,
|
||||
INTEGRATION_BRANCH_REF,
|
||||
};
|
||||
use gitbutler_project::access::WorktreeWritePermission;
|
||||
use gitbutler_reference::{ReferenceName, Refname};
|
||||
use gitbutler_repo::{
|
||||
rebase::{cherry_rebase, cherry_rebase_group},
|
||||
RepositoryExt,
|
||||
};
|
||||
|
||||
pub mod commands;
|
||||
|
||||
pub const EDIT_UNCOMMITED_FILES_REF: &str = "refs/gitbutler/edit_uncommited_files";
|
||||
|
||||
fn save_uncommited_files(ctx: &CommandContext) -> Result<()> {
|
||||
let repository = ctx.repository();
|
||||
|
||||
// Create a tree of all uncommited files
|
||||
let mut index = repository.index().context("Failed to get index")?;
|
||||
index
|
||||
.add_all(["*"], git2::IndexAddOption::DEFAULT, None)
|
||||
.context("Failed to add all to index")?;
|
||||
index.write().context("Failed to write index")?;
|
||||
let tree_oid = index
|
||||
.write_tree()
|
||||
.context("Failed to create tree from index")?;
|
||||
let tree = repository
|
||||
.find_tree(tree_oid)
|
||||
.context("Failed to find tree")?;
|
||||
|
||||
// Commit tree and reference it
|
||||
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.head().context("Failed to get head")?;
|
||||
let head_commit = head.peel_to_commit().context("Failed to get head commit")?;
|
||||
let commit = repository
|
||||
.commit(
|
||||
None,
|
||||
&author_signature,
|
||||
&committer_signature,
|
||||
"Edit mode saved changes",
|
||||
&tree,
|
||||
&[&head_commit],
|
||||
)
|
||||
.context("Failed to write stash commit")?;
|
||||
|
||||
repository
|
||||
.reference(EDIT_UNCOMMITED_FILES_REF, commit, true, "")
|
||||
.context("Failed to reference uncommited files")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checkout_edit_branch(ctx: &CommandContext, editee: &git2::Commit) -> Result<()> {
|
||||
let repository = ctx.repository();
|
||||
|
||||
// Checkout editee's parent
|
||||
let editee_parent = editee.parent(0).context("Failed to get editee's parent")?;
|
||||
repository
|
||||
.reference(EDIT_BRANCH_REF, editee_parent.id(), true, "")
|
||||
.context("Failed to update edit branch reference")?;
|
||||
repository
|
||||
.set_head(EDIT_BRANCH_REF)
|
||||
.context("Failed to set head reference")?;
|
||||
repository
|
||||
.checkout_head(Some(CheckoutBuilder::new().force().remove_untracked(true)))
|
||||
.context("Failed to checkout head")?;
|
||||
|
||||
// Checkout the editee as unstaged changes
|
||||
let editee_tree = editee.tree().context("Failed to get editee's tree")?;
|
||||
repository
|
||||
.checkout_tree(
|
||||
editee_tree.as_object(),
|
||||
Some(CheckoutBuilder::new().force().remove_untracked(true)),
|
||||
)
|
||||
.context("Failed to checkout editee")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_virtual_branch_by_reference(
|
||||
ctx: &CommandContext,
|
||||
reference: &ReferenceName,
|
||||
) -> Result<Option<Branch>> {
|
||||
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
|
||||
let all_virtual_branches = vb_state
|
||||
.list_branches_in_workspace()
|
||||
.context("Failed to read virtual branches")?;
|
||||
|
||||
Ok(all_virtual_branches.into_iter().find(|virtual_branch| {
|
||||
let Ok(refname) = virtual_branch.refname() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(editee_refname) = Refname::from_str(reference) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
editee_refname == refname.into()
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn enter_edit_mode(
|
||||
ctx: &CommandContext,
|
||||
editee: &git2::Commit,
|
||||
editee_branch: &git2::Reference,
|
||||
_perm: &mut WorktreeWritePermission,
|
||||
) -> Result<EditModeMetadata> {
|
||||
let Some(editee_branch) = editee_branch.name() else {
|
||||
bail!("Failed to get editee branch name");
|
||||
};
|
||||
|
||||
let edit_mode_metadata = EditModeMetadata {
|
||||
editee_commit_sha: editee.id(),
|
||||
editee_branch: editee_branch.to_string().into(),
|
||||
};
|
||||
|
||||
if find_virtual_branch_by_reference(ctx, &edit_mode_metadata.editee_branch)?.is_none() {
|
||||
bail!("Can not enter edit mode for a reference which does not have a cooresponding virtual branch")
|
||||
}
|
||||
|
||||
save_uncommited_files(ctx).context("Failed to save uncommited files")?;
|
||||
checkout_edit_branch(ctx, editee).context("Failed to checkout edit branch")?;
|
||||
write_edit_mode_metadata(ctx, &edit_mode_metadata).context("Failed to persist metadata")?;
|
||||
|
||||
Ok(edit_mode_metadata)
|
||||
}
|
||||
|
||||
pub(crate) fn save_and_return_to_workspace(
|
||||
ctx: &CommandContext,
|
||||
perm: &mut WorktreeWritePermission,
|
||||
) -> Result<()> {
|
||||
let edit_mode_metadata = read_edit_mode_metadata(ctx).context("Failed to read metadata")?;
|
||||
let repository = ctx.repository();
|
||||
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
|
||||
|
||||
// Get important references
|
||||
let editee = repository
|
||||
.find_commit(edit_mode_metadata.editee_commit_sha)
|
||||
.context("Failed to find editee")?;
|
||||
let editee_parent = editee.parent(0).context("Failed to get editee's parent")?;
|
||||
let stashed_integration_changes_reference = repository
|
||||
.find_reference(EDIT_UNCOMMITED_FILES_REF)
|
||||
.context("Failed to find stashed integration changes")?;
|
||||
let stashed_integration_changes_commit = stashed_integration_changes_reference
|
||||
.peel_to_commit()
|
||||
.context("Failed to get stashed changes commit")?;
|
||||
|
||||
let Some(mut editee_virtual_branch) =
|
||||
find_virtual_branch_by_reference(ctx, &edit_mode_metadata.editee_branch)?
|
||||
else {
|
||||
bail!("Failed to find virtual branch for this reference. Entering and leaving edit mode for non-virtual branches is unsupported")
|
||||
};
|
||||
|
||||
// Recommit editee
|
||||
let mut index = repository.index().context("Failed to get index")?;
|
||||
index
|
||||
.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
|
||||
.context("Failed to add all to index")?;
|
||||
index.write().context("Failed to write index")?;
|
||||
let tree_oid = index
|
||||
.write_tree()
|
||||
.context("Failed to create tree from index")?;
|
||||
let tree = repository
|
||||
.find_tree(tree_oid)
|
||||
.context("Failed to find tree")?;
|
||||
let new_editee_oid = ctx
|
||||
.repository()
|
||||
.commit_with_signature(
|
||||
None,
|
||||
&editee.author(),
|
||||
&editee.committer(),
|
||||
&editee.message_bstr().to_str_lossy(),
|
||||
&tree,
|
||||
&[&editee_parent],
|
||||
editee.gitbutler_headers(),
|
||||
)
|
||||
.context("Failed to commit new editee")?;
|
||||
|
||||
// Rebase all all commits on top of the new editee and update reference
|
||||
let new_editee_branch_head =
|
||||
cherry_rebase(ctx, new_editee_oid, editee.id(), editee_virtual_branch.head)
|
||||
.context("Failed to rebase commits onto new editee")?
|
||||
.unwrap_or(new_editee_oid);
|
||||
repository
|
||||
.reference(
|
||||
&edit_mode_metadata.editee_branch,
|
||||
new_editee_branch_head,
|
||||
true,
|
||||
"",
|
||||
)
|
||||
.context("Failed to reference new editee branch head")?;
|
||||
|
||||
// Move back to gitbutler/integration and restore stashed changes
|
||||
{
|
||||
repository
|
||||
.set_head(INTEGRATION_BRANCH_REF)
|
||||
.context("Failed to set head reference")?;
|
||||
repository
|
||||
.checkout_head(Some(CheckoutBuilder::new().force().remove_untracked(true)))
|
||||
.context("Failed to checkout gitbutler/integration")?;
|
||||
|
||||
editee_virtual_branch.head = new_editee_branch_head;
|
||||
editee_virtual_branch.updated_timestamp_ms = gitbutler_time::time::now_ms();
|
||||
vb_state
|
||||
.set_branch(editee_virtual_branch)
|
||||
.context("Failed to update vbstate")?;
|
||||
|
||||
let integration_commit_oid = update_gitbutler_integration(&vb_state, ctx)
|
||||
.context("Failed to update gitbutler integration")?;
|
||||
|
||||
let rebased_stashed_integration_changes_commit = cherry_rebase_group(
|
||||
ctx,
|
||||
integration_commit_oid,
|
||||
&mut [stashed_integration_changes_commit.id()],
|
||||
)
|
||||
.context("Failed to rebase stashed integration commit changes")?;
|
||||
|
||||
let commit_thing = repository
|
||||
.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()
|
||||
.context("Failed to get tree of commit of rebased stashed integration changes")?;
|
||||
|
||||
repository
|
||||
.checkout_tree(
|
||||
tree_thing.as_object(),
|
||||
Some(CheckoutBuilder::new().force().remove_untracked(true)),
|
||||
)
|
||||
.context("Failed to checkout stashed changes tree")?;
|
||||
|
||||
list_virtual_branches(ctx, perm).context("Failed to list virtual branches")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -42,6 +42,7 @@ pub fn write_edit_mode_metadata(
|
||||
|
||||
/// Holds relevant state required to switch to and from edit mode
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditModeMetadata {
|
||||
/// The sha of the commit getting edited.
|
||||
#[serde(with = "gitbutler_serde::oid")]
|
||||
|
@ -9,14 +9,18 @@ use crate::{LogUntil, RepoActionsExt, RepositoryExt};
|
||||
/// cherry-pick based rebase, which handles empty commits
|
||||
/// this function takes a commit range and generates a Vector of commit oids
|
||||
/// and then passes them to `cherry_rebase_group` to rebase them onto the target commit
|
||||
///
|
||||
/// Returns the new head commit id
|
||||
pub fn cherry_rebase(
|
||||
ctx: &CommandContext,
|
||||
target_commit_oid: git2::Oid,
|
||||
start_commit_oid: git2::Oid,
|
||||
end_commit_oid: git2::Oid,
|
||||
to_commit_oid: git2::Oid,
|
||||
from_commit_oid: git2::Oid,
|
||||
) -> Result<Option<git2::Oid>> {
|
||||
// get a list of the commits to rebase
|
||||
let mut ids_to_rebase = ctx.l(end_commit_oid, LogUntil::Commit(start_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() {
|
||||
return Ok(None);
|
||||
|
@ -64,6 +64,7 @@ gitbutler-id.workspace = true
|
||||
gitbutler-storage.workspace = true
|
||||
gitbutler-diff.workspace = true
|
||||
gitbutler-operating-modes.workspace = true
|
||||
gitbutler-edit-mode.workspace = true
|
||||
open = "5"
|
||||
|
||||
[dependencies.tauri]
|
||||
|
@ -195,6 +195,8 @@ fn main() {
|
||||
remotes::list_remotes,
|
||||
remotes::add_remote,
|
||||
modes::operating_mode,
|
||||
modes::enter_edit_mode,
|
||||
modes::save_edit_and_return_to_workspace
|
||||
])
|
||||
.menu(menu::build(tauri_context.package_info()))
|
||||
.on_menu_event(|event| menu::handle_event(&event))
|
||||
|
@ -1,11 +1,15 @@
|
||||
use anyhow::Context;
|
||||
use gitbutler_operating_modes::EditModeMetadata;
|
||||
use gitbutler_operating_modes::OperatingMode;
|
||||
use gitbutler_project::Controller;
|
||||
use gitbutler_project::ProjectId;
|
||||
use tauri::State;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(projects), err(Debug))]
|
||||
pub fn operating_mode(
|
||||
projects: State<'_, Controller>,
|
||||
project_id: ProjectId,
|
||||
@ -13,3 +17,30 @@ pub fn operating_mode(
|
||||
let project = projects.get(project_id)?;
|
||||
gitbutler_operating_modes::commands::operating_mode(&project).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(projects), err(Debug))]
|
||||
pub fn enter_edit_mode(
|
||||
projects: State<'_, Controller>,
|
||||
project_id: ProjectId,
|
||||
editee: String,
|
||||
editee_branch: String,
|
||||
) -> Result<EditModeMetadata, Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
|
||||
let editee = git2::Oid::from_str(&editee).context("Failed to parse editee oid")?;
|
||||
|
||||
gitbutler_edit_mode::commands::enter_edit_mode(&project, editee, editee_branch.into())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(projects), err(Debug))]
|
||||
pub fn save_edit_and_return_to_workspace(
|
||||
projects: State<'_, Controller>,
|
||||
project_id: ProjectId,
|
||||
) -> Result<(), Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
|
||||
gitbutler_edit_mode::commands::save_and_return_to_workspace(&project).map_err(Into::into)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user