mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-26 12:24:26 +03:00
Somethign somethign matchy matchy design
This commit is contained in:
parent
d2d3afbf2b
commit
3fb5077c4a
15
Cargo.lock
generated
15
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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::{
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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};
|
||||
|
11
crates/gitbutler-cherry-pick/Cargo.toml
Normal file
11
crates/gitbutler-cherry-pick/Cargo.toml
Normal 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
|
104
crates/gitbutler-cherry-pick/src/lib.rs
Normal file
104
crates/gitbutler-cherry-pick/src/lib.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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()?;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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]]
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
@ -16,5 +16,3 @@ mod config;
|
||||
pub use config::Config;
|
||||
|
||||
pub mod askpass;
|
||||
|
||||
mod conflicts;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user