Somethign somethign matchy matchy design

This commit is contained in:
Caleb Owens 2024-08-26 19:23:15 +02:00
parent d2d3afbf2b
commit 3fb5077c4a
No known key found for this signature in database
24 changed files with 460 additions and 174 deletions

15
Cargo.lock generated
View File

@ -2140,6 +2140,7 @@ dependencies = [
"git2",
"git2-hooks",
"gitbutler-branch",
"gitbutler-cherry-pick",
"gitbutler-command-context",
"gitbutler-commit",
"gitbutler-diff",
@ -2175,6 +2176,15 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "gitbutler-cherry-pick"
version = "0.0.0"
dependencies = [
"anyhow",
"git2",
"gitbutler-commit",
]
[[package]]
name = "gitbutler-cli"
version = "0.0.0"
@ -2235,6 +2245,7 @@ dependencies = [
"bstr",
"diffy",
"git2",
"gitbutler-cherry-pick",
"gitbutler-command-context",
"gitbutler-serde",
"hex",
@ -2252,14 +2263,17 @@ dependencies = [
"git2",
"gitbutler-branch",
"gitbutler-branch-actions",
"gitbutler-cherry-pick",
"gitbutler-command-context",
"gitbutler-commit",
"gitbutler-diff",
"gitbutler-operating-modes",
"gitbutler-oplog",
"gitbutler-project",
"gitbutler-reference",
"gitbutler-repo",
"gitbutler-time",
"serde",
]
[[package]]
@ -2419,6 +2433,7 @@ dependencies = [
"bstr",
"git2",
"gitbutler-branch",
"gitbutler-cherry-pick",
"gitbutler-command-context",
"gitbutler-commit",
"gitbutler-config",

View File

@ -30,6 +30,7 @@ members = [
"crates/gitbutler-diff",
"crates/gitbutler-operating-modes",
"crates/gitbutler-edit-mode",
"crates/gitbutler-cherry-pick",
]
resolver = "2"
@ -82,6 +83,7 @@ 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" }
gitbutler-cherry-pick = { path = "crates/gitbutler-cherry-pick" }
[profile.release]
codegen-units = 1 # Compile crates one after another so the compiler can optimize better

View File

@ -3,9 +3,12 @@
import ProjectNameLabel from '../shared/ProjectNameLabel.svelte';
import newProjectSvg from '$lib/assets/illustrations/new-project.svg?raw';
import { Project } from '$lib/backend/projects';
import { ModeService, type EditModeMetadata } from '$lib/modes/service';
import { InitialFile, ModeService, type EditModeMetadata } from '$lib/modes/service';
import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher';
import { getContext } from '$lib/utils/context';
import Button from '@gitbutler/ui/Button.svelte';
import FileListItem from '@gitbutler/ui/file/FileListItem.svelte';
import type { FileStatus } from '@gitbutler/ui/file/types';
interface Props {
editModeMetadata: EditModeMetadata;
@ -14,10 +17,77 @@
const { editModeMetadata }: Props = $props();
const project = getContext(Project);
const uncommitedFileWatcher = getContext(UncommitedFilesWatcher);
const modeService = getContext(ModeService);
const uncommitedFiles = uncommitedFileWatcher.uncommitedFiles;
let modeServiceSaving = $state<'inert' | 'loading' | 'completed'>('inert');
let initialFiles = $state<InitialFile[]>([]);
$effect(() => {
modeService.getInitialIndexState().then((files) => {
initialFiles = files;
});
});
interface FileEntry {
name: string;
path: string;
conflicted: boolean;
status?: FileStatus;
}
const files = $derived.by(() => {
const files: FileEntry[] = initialFiles.map((initialFile) => ({
name: initialFile.filename,
path: initialFile.filePath,
conflicted: initialFile.conflicted,
status: $uncommitedFiles.some(
(uncommitedFile) => uncommitedFile[0].path === initialFile.filePath
)
? undefined
: 'D'
}));
console.log(initialFiles);
$uncommitedFiles.forEach((uncommitedFile) => {
console.log(uncommitedFile);
const foundFile = files.find((file) => file.path === uncommitedFile[0].path);
if (foundFile) {
const initialFile = initialFiles.find(
(initialFile) => initialFile.filePath === foundFile.path
)!;
// This may incorrectly be true if the file is conflicted
// To compensate for this, we also check if the uncommited diff
// has conflict markers.
const fileChanged = initialFile.file.hunks.some(
(hunk) => !uncommitedFile[0].hunks.map((hunk) => hunk.diff).includes(hunk.diff)
);
if (fileChanged && !uncommitedFile[0].looksConflicted) {
foundFile.status = 'M';
foundFile.conflicted = false;
}
} else {
files.push({
name: uncommitedFile[0].filename,
path: uncommitedFile[0].path,
conflicted: false,
status: 'A'
});
}
});
files.sort((a, b) => a.path.localeCompare(b.path));
return files;
});
async function abort() {
modeServiceSaving = 'loading';
@ -51,6 +121,21 @@
actions.
</p>
<div class="files">
<p class="text-13 text-semibold header">Commit files</p>
{#each files as file}
<div class="file">
<FileListItem
fileName={file.name}
filePath={file.path}
fileStatus={file.status}
conflicted={file.conflicted}
fileStatusStyle={'full'}
/>
</div>
{/each}
</div>
<div class="switchrepo__actions">
<Button style="ghost" outline onclick={abort} loading={modeServiceSaving === 'loading'}>
Cancel changes
@ -89,4 +174,28 @@
flex-wrap: wrap;
justify-content: flex-end;
}
.files {
border: 1px solid var(--clr-border-2);
border-radius: var(--radius-m);
margin-bottom: 16px;
overflow: hidden;
padding-bottom: 8px;
& .header {
margin-left: 16px;
margin-top: 16px;
margin-bottom: 8px;
}
& .file {
border-bottom: 1px solid var(--clr-border-3);
&:last-child {
border-bottom: none;
}
}
}
</style>

View File

@ -1,5 +1,8 @@
import 'reflect-metadata';
import { invoke, listen } from '$lib/backend/ipc';
import { plainToInstance, Type } from 'class-transformer';
import { derived, writable } from 'svelte/store';
import { RemoteFile } from '$lib/vbranches/types';
export interface EditModeMetadata {
commitOid: string;
@ -18,6 +21,18 @@ interface HeadAndMode {
operatingMode?: Mode;
}
export class InitialFile {
filePath!: string;
conflicted!: boolean;
@Type(() => RemoteFile)
file!: RemoteFile;
get filename(): string {
const parts = this.filePath.split('/');
return parts.at(-1) ?? this.filePath;
}
}
export class ModeService {
private headAndMode = writable<HeadAndMode>({}, (set) => {
this.refresh();
@ -60,6 +75,13 @@ export class ModeService {
projectId: this.projectId
});
}
async getInitialIndexState() {
return plainToInstance(
InitialFile,
await invoke<unknown[]>('edit_initial_index_state', { projectId: this.projectId })
);
}
}
function subscribeToHead(projectId: string, callback: (headAndMode: HeadAndMode) => void) {

View File

@ -292,6 +292,20 @@ export class RemoteFile {
get locked(): boolean {
return false;
}
get looksConflicted(): boolean {
const hasStartingMarker = this.hunks.some((hunk) =>
hunk.diff.split('\n').some((line) => line.startsWith('>>>>>>> theirs', 1))
);
const hasEndingMarker = this.hunks.some((hunk) =>
hunk.diff.split('\n').some((line) => line.startsWith('<<<<<<< ours', 1))
);
console.log(this.hunks);
return hasStartingMarker && hasEndingMarker;
}
}
export interface Author {

View File

@ -25,6 +25,7 @@ gitbutler-url.workspace = true
gitbutler-fs.workspace = true
gitbutler-diff.workspace = true
gitbutler-operating-modes.workspace = true
gitbutler-cherry-pick.workspace = true
serde = { workspace = true, features = ["std"] }
bstr.workspace = true
diffy = "0.4.0"
@ -56,4 +57,4 @@ benches = ["gitbutler-git/benches"]
[[bench]]
name = "branches"
harness = false
harness = false

View File

@ -4,9 +4,9 @@ use std::{
};
use anyhow::{anyhow, Context, Result};
use gitbutler_cherry_pick::RepositoryExt as _;
use gitbutler_command_context::CommandContext;
use gitbutler_diff::FileDiff;
use gitbutler_repo::RepositoryExt;
use serde::Serialize;
use crate::{

View File

@ -6,6 +6,7 @@ use gitbutler_branch::{
self, Branch, BranchCreateRequest, SignaturePurpose, VirtualBranchesHandle,
GITBUTLER_INTEGRATION_REFERENCE,
};
use gitbutler_cherry_pick::RepositoryExt as _;
use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_error::error::Marker;

View File

@ -5,11 +5,11 @@ use git2::Tree;
use gitbutler_branch::{
Branch, BranchCreateRequest, BranchId, BranchOwnershipClaims, OwnershipClaim,
};
use gitbutler_cherry_pick::RepositoryExt as _;
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,

View File

@ -12,6 +12,7 @@ use gitbutler_branch::{
dedup, dedup_fmt, reconcile_claims, signature, Branch, BranchId, BranchOwnershipClaims,
BranchUpdateRequest, OwnershipClaim, SignaturePurpose, Target, VirtualBranchesHandle,
};
use gitbutler_cherry_pick::RepositoryExt as _;
use gitbutler_command_context::CommandContext;
use gitbutler_commit::{commit_ext::CommitExt, commit_headers::HasCommitHeaders};
use gitbutler_diff::{trees, GitHunk, Hunk};

View File

@ -0,0 +1,11 @@
[package]
name = "gitbutler-cherry-pick"
version = "0.0.0"
edition = "2021"
authors = ["GitButler <gitbutler@gitbutler.com>"]
publish = false
[dependencies]
gitbutler-commit.workspace = true
git2.workspace = true
anyhow.workspace = true

View File

@ -0,0 +1,104 @@
// 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;
use anyhow::Context;
use gitbutler_commit::commit_ext::CommitExt;
#[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",
}
}
}
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>;
}
impl RepositoryExt for git2::Repository {
/// 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() {
// Use to_rebase's recorded base
self.find_real_tree(to_rebase, ConflictedTreeKey::Base)?
} else {
let base_commit = to_rebase.parent(0)?;
// Use the parent's auto-resolution
self.find_real_tree(&base_commit, Default::default())?
};
// Get the auto-resolution
let ours = self.find_real_tree(head, Default::default())?;
// 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")
}
}
}

View File

@ -14,8 +14,9 @@ hex = "0.4.3"
tracing.workspace = true
gitbutler-serde.workspace = true
gitbutler-command-context.workspace = true
gitbutler-cherry-pick.workspace = true
diffy = "0.4.0"
serde = { workspace = true, features = ["std"]}
serde = { workspace = true, features = ["std"] }
[[test]]
name = "diff"

View File

@ -7,6 +7,7 @@ use std::{
use anyhow::{Context, Result};
use bstr::{BStr, BString, ByteSlice, ByteVec};
use gitbutler_cherry_pick::RepositoryExt;
use gitbutler_serde::BStringForFrontend;
use serde::{Deserialize, Serialize};
use tracing::instrument;
@ -130,7 +131,7 @@ pub fn workdir(repo: &git2::Repository, commit_oid: &git2::Oid) -> Result<DiffBy
let commit = repo
.find_commit(*commit_oid)
.context("failed to find commit")?;
let old_tree = commit.tree().context("failed to find tree")?;
let old_tree = repo.find_real_tree(&commit, Default::default())?;
let mut workdir_index = repo.index()?;

View File

@ -19,3 +19,6 @@ gitbutler-branch-actions.workspace = true
gitbutler-reference.workspace = true
gitbutler-time.workspace = true
gitbutler-oplog.workspace = true
gitbutler-diff.workspace = true
gitbutler-cherry-pick.workspace = true
serde.workspace = true

View File

@ -8,6 +8,8 @@ use gitbutler_oplog::{
use gitbutler_project::{access::WriteWorkspaceGuard, Project};
use gitbutler_reference::ReferenceName;
use crate::InitialFile;
pub fn enter_edit_mode(
project: &Project,
commit_oid: git2::Oid,
@ -60,6 +62,14 @@ pub fn abort_and_return_to_workspace(project: &Project) -> Result<()> {
crate::abort_and_return_to_workspace(&ctx, guard.write_permission())
}
pub fn starting_index_state(project: &Project) -> Result<Vec<InitialFile>> {
let (ctx, guard) = open_with_permission(project)?;
assure_edit_mode(&ctx)?;
crate::starting_index_state(&ctx, guard.read_permission())
}
fn open_with_permission(project: &Project) -> Result<(CommandContext, WriteWorkspaceGuard)> {
let ctx = CommandContext::open(project)?;
let guard = project.exclusive_worktree_access();

View File

@ -1,25 +1,30 @@
use std::str::FromStr;
use std::{path::PathBuf, 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_branch_actions::{
list_virtual_branches, update_gitbutler_integration, RemoteBranchFile,
};
use gitbutler_cherry_pick::RepositoryExt as _;
use gitbutler_command_context::CommandContext;
use gitbutler_commit::{
commit_ext::CommitExt,
commit_headers::{CommitHeadersV2, HasCommitHeaders},
};
use gitbutler_diff::hunks_by_filepath;
use gitbutler_operating_modes::{
read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata, EDIT_BRANCH_REF,
INTEGRATION_BRANCH_REF,
operating_mode, read_edit_mode_metadata, write_edit_mode_metadata, EditModeMetadata,
OperatingMode, EDIT_BRANCH_REF, INTEGRATION_BRANCH_REF,
};
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_project::access::{WorktreeReadPermission, WorktreeWritePermission};
use gitbutler_reference::{ReferenceName, Refname};
use gitbutler_repo::{
rebase::{cherry_rebase, cherry_rebase_group},
RepositoryExt,
};
use serde::Serialize;
pub mod commands;
@ -66,21 +71,7 @@ fn save_uncommited_files(ctx: &CommandContext) -> Result<()> {
Ok(())
}
fn checkout_edit_branch(ctx: &CommandContext, commit: &git2::Commit) -> Result<()> {
let repository = ctx.repository();
// Checkout commits's parent
let commit_parent = commit.parent(0).context("Failed to get commit's parent")?;
repository
.reference(EDIT_BRANCH_REF, commit_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")?;
fn get_commit_index(repository: &git2::Repository, commit: &git2::Commit) -> Result<git2::Index> {
let commit_tree = commit.tree().context("Failed to get commit's tree")?;
// Checkout the commit as unstaged changes
if commit.is_conflicted() {
@ -105,29 +96,50 @@ fn checkout_edit_branch(ctx: &CommandContext, commit: &git2::Commit) -> Result<(
.find_tree(theirs.id())
.context("Failed to find base tree")?;
let mut index = repository
let 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")?;
Ok(index)
} else {
repository
.checkout_tree(
commit_tree.as_object(),
Some(CheckoutBuilder::new().force().remove_untracked(true)),
)
.context("Failed to checkout commit")?;
};
let mut index = git2::Index::new()?;
index
.read_tree(&commit_tree)
.context("Failed to set index tree")?;
Ok(index)
}
}
fn checkout_edit_branch(ctx: &CommandContext, commit: &git2::Commit) -> Result<()> {
let repository = ctx.repository();
// Checkout commits's parent
let commit_parent = commit.parent(0).context("Failed to get commit's parent")?;
repository
.reference(EDIT_BRANCH_REF, commit_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 commit as unstaged changes
let mut index = get_commit_index(repository, commit)?;
repository
.checkout_index(
Some(&mut index),
Some(
CheckoutBuilder::new()
.force()
.remove_untracked(true)
.conflict_style_diff3(true),
),
)
.context("Failed to checkout conflicted commit")?;
Ok(())
}
@ -331,3 +343,69 @@ pub(crate) fn save_and_return_to_workspace(
Ok(())
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InitialFile {
file_path: PathBuf,
conflicted: bool,
file: RemoteBranchFile,
}
pub(crate) fn starting_index_state(
ctx: &CommandContext,
_perm: &WorktreeReadPermission,
) -> Result<Vec<InitialFile>> {
let OperatingMode::Edit(metadata) = operating_mode(ctx) else {
bail!("Starting index state can only be fetched while in edit mode")
};
let repository = ctx.repository();
let commit = repository.find_commit(metadata.commit_oid)?;
let commit_parent = commit.parent(0)?;
let commit_parent_tree = repository.find_real_tree(&commit_parent, Default::default())?;
let edit_mode_index = get_commit_index(repository, &commit)?;
let diff =
repository.diff_tree_to_index(Some(&commit_parent_tree), Some(&edit_mode_index), None)?;
let mut index_files = vec![];
let diff_files = hunks_by_filepath(Some(repository), &diff)?;
diff.foreach(
&mut |delta, _| {
let conflicted = delta.status() == git2::Delta::Conflicted;
let Some(path) = delta.new_file().path() else {
// Ignore file
return true;
};
let Some(file) = diff_files.get(path) else {
return true;
};
let binary = file.hunks.iter().any(|h| h.binary);
let remote_file = RemoteBranchFile {
path: path.into(),
hunks: file.hunks.clone(),
binary,
};
index_files.push(InitialFile {
conflicted,
file_path: path.into(),
file: remote_file,
});
true
},
None,
None,
None,
)?;
Ok(index_files)
}

View File

@ -34,6 +34,7 @@ gitbutler-id.workspace = true
gitbutler-time.workspace = true
gitbutler-commit.workspace = true
gitbutler-url.workspace = true
gitbutler-cherry-pick.workspace = true
uuid.workspace = true
[[test]]

View File

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

View File

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

View File

@ -1,6 +1,7 @@
use anyhow::{anyhow, Context, Result};
use bstr::ByteSlice;
use git2::{build::TreeUpdateBuilder, Repository};
use gitbutler_cherry_pick::{ConflictedTreeKey, RepositoryExt};
use gitbutler_command_context::CommandContext;
use gitbutler_commit::{
commit_ext::CommitExt,
@ -10,7 +11,7 @@ use gitbutler_error::error::Marker;
use tempfile::tempdir;
use uuid::Uuid;
use crate::{conflicts::ConflictedTreeKey, LogUntil, RepoActionsExt, RepositoryExt};
use crate::{LogUntil, RepoActionsExt};
/// cherry-pick based rebase, which handles empty commits
/// this function takes a commit range and generates a Vector of commit oids
@ -111,17 +112,17 @@ fn commit_unconflicted_cherry_result<'repository>(
..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")?;
let commit_oid = crate::RepositoryExt::commit_with_signature(
repository,
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)
@ -246,19 +247,19 @@ fn commit_conflicted_cherry_result<'repository>(
});
// 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")?;
let commit_oid = crate::RepositoryExt::commit_with_signature(
repository,
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
{

View File

@ -7,30 +7,16 @@ 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_ext::CommitExt, commit_headers::CommitHeadersV2,
};
use gitbutler_commit::{commit_buffer::CommitBuffer, 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.
@ -389,56 +375,6 @@ 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() {
// Use to_rebase's recorded base
self.find_real_tree(to_rebase, ConflictedTreeKey::Base)?
} else {
let base_commit = to_rebase.parent(0)?;
// Use the parent's auto-resolution
self.find_real_tree(&base_commit, Default::default())?
};
// Get the auto-resolution
let ours = self.find_real_tree(head, Default::default())?;
// 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.

View File

@ -199,7 +199,8 @@ fn main() {
modes::operating_mode,
modes::enter_edit_mode,
modes::save_edit_and_return_to_workspace,
modes::abort_edit_and_return_to_workspace
modes::abort_edit_and_return_to_workspace,
modes::edit_initial_index_state
])
.menu(menu::build(tauri_context.package_info()))
.on_menu_event(|event| menu::handle_event(&event))

View File

@ -1,4 +1,5 @@
use anyhow::Context;
use gitbutler_edit_mode::InitialFile;
use gitbutler_operating_modes::EditModeMetadata;
use gitbutler_operating_modes::OperatingMode;
use gitbutler_project::Controller;
@ -55,3 +56,14 @@ pub fn save_edit_and_return_to_workspace(
gitbutler_edit_mode::commands::save_and_return_to_workspace(&project).map_err(Into::into)
}
#[tauri::command(async)]
#[instrument(skip(projects), err(Debug))]
pub fn edit_initial_index_state(
projects: State<'_, Controller>,
project_id: ProjectId,
) -> Result<Vec<InitialFile>, Error> {
let project = projects.get(project_id)?;
gitbutler_edit_mode::commands::starting_index_state(&project).map_err(Into::into)
}