mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-23 03:26:36 +03:00
Merge pull request #4760 from gitbutlerapp/Show-files-in-edit-mode
Show files in edit mode
This commit is contained in:
commit
a7399ab083
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
|
||||
|
@ -4,8 +4,12 @@
|
||||
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 { 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 { RemoteFile } from '$lib/vbranches/types';
|
||||
import type { FileStatus } from '@gitbutler/ui/file/types';
|
||||
|
||||
interface Props {
|
||||
editModeMetadata: EditModeMetadata;
|
||||
@ -14,16 +18,95 @@
|
||||
const { editModeMetadata }: Props = $props();
|
||||
|
||||
const project = getContext(Project);
|
||||
const uncommitedFileWatcher = getContext(UncommitedFilesWatcher);
|
||||
const modeService = getContext(ModeService);
|
||||
|
||||
const uncommitedFiles = uncommitedFileWatcher.uncommitedFiles;
|
||||
|
||||
let modeServiceAborting = $state<'inert' | 'loading' | 'completed'>('inert');
|
||||
let modeServiceSaving = $state<'inert' | 'loading' | 'completed'>('inert');
|
||||
|
||||
let initialFiles = $state<RemoteFile[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
modeService.getInitialIndexState().then((files) => {
|
||||
initialFiles = files;
|
||||
});
|
||||
});
|
||||
|
||||
interface FileEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
conflicted: boolean;
|
||||
status?: FileStatus;
|
||||
}
|
||||
|
||||
const files = $derived.by(() => {
|
||||
const initialFileMap = new Map<string, RemoteFile>();
|
||||
const uncommitedFileMap = new Map<string, RemoteFile>();
|
||||
const outputMap = new Map<string, FileEntry>();
|
||||
|
||||
// Build maps of files
|
||||
{
|
||||
initialFiles.forEach((initialFile) => {
|
||||
initialFileMap.set(initialFile.path, initialFile);
|
||||
});
|
||||
|
||||
$uncommitedFiles.forEach(([uncommitedFile]) => {
|
||||
uncommitedFileMap.set(uncommitedFile.path, uncommitedFile);
|
||||
});
|
||||
}
|
||||
|
||||
// Create output
|
||||
{
|
||||
initialFiles.forEach((initialFile) => {
|
||||
const isDeleted = uncommitedFileMap.has(initialFile.path);
|
||||
|
||||
outputMap.set(initialFile.path, {
|
||||
name: initialFile.filename,
|
||||
path: initialFile.path,
|
||||
conflicted: initialFile.looksConflicted,
|
||||
status: isDeleted ? undefined : 'D'
|
||||
});
|
||||
});
|
||||
|
||||
$uncommitedFiles.forEach(([uncommitedFile]) => {
|
||||
const initialFile = initialFileMap.get(uncommitedFile.path);
|
||||
if (initialFile) {
|
||||
const fileChanged = initialFile.hunks.some(
|
||||
(hunk) => !uncommitedFile.hunks.map((hunk) => hunk.diff).includes(hunk.diff)
|
||||
);
|
||||
|
||||
if (fileChanged && !uncommitedFile.looksConflicted) {
|
||||
// All initial entries should have been added to the map,
|
||||
// so we can safly assert that it will be present
|
||||
const outputFile = outputMap.get(uncommitedFile.path)!;
|
||||
outputFile.status = 'M';
|
||||
outputFile.conflicted = false;
|
||||
}
|
||||
} else {
|
||||
outputMap.set(uncommitedFile.path, {
|
||||
name: uncommitedFile.filename,
|
||||
path: uncommitedFile.path,
|
||||
conflicted: false,
|
||||
status: 'A'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const files = Array.from(outputMap.values());
|
||||
files.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
return files;
|
||||
});
|
||||
|
||||
async function abort() {
|
||||
modeServiceSaving = 'loading';
|
||||
modeServiceAborting = 'loading';
|
||||
|
||||
await modeService.abortEditAndReturnToWorkspace();
|
||||
|
||||
modeServiceSaving = 'completed';
|
||||
modeServiceAborting = 'completed';
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@ -51,8 +134,23 @@
|
||||
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'}>
|
||||
<Button style="ghost" outline onclick={abort} loading={modeServiceAborting === 'loading'}>
|
||||
Cancel changes
|
||||
</Button>
|
||||
<Button
|
||||
@ -89,4 +187,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,4 +1,6 @@
|
||||
import { invoke, listen } from '$lib/backend/ipc';
|
||||
import { RemoteFile } from '$lib/vbranches/types';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export interface EditModeMetadata {
|
||||
@ -60,6 +62,13 @@ export class ModeService {
|
||||
projectId: this.projectId
|
||||
});
|
||||
}
|
||||
|
||||
async getInitialIndexState() {
|
||||
return plainToInstance(
|
||||
RemoteFile,
|
||||
await invoke<unknown[]>('edit_initial_index_state', { projectId: this.projectId })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToHead(projectId: string, callback: (headAndMode: HeadAndMode) => void) {
|
||||
|
48
apps/desktop/src/lib/uncommitedFiles/watcher.ts
Normal file
48
apps/desktop/src/lib/uncommitedFiles/watcher.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { listen } from '$lib/backend/ipc';
|
||||
import { parseRemoteFiles } from '$lib/vbranches/remoteCommits';
|
||||
import { RemoteFile } from '$lib/vbranches/types';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import type { Project } from '$lib/backend/projects';
|
||||
import type { ContentSection, HunkSection } from '$lib/utils/fileSections';
|
||||
|
||||
type ParsedFiles = [RemoteFile, (ContentSection | HunkSection)[]][];
|
||||
|
||||
export class UncommitedFilesWatcher {
|
||||
uncommitedFiles: Readable<ParsedFiles>;
|
||||
|
||||
constructor(private project: Project) {
|
||||
this.uncommitedFiles = readable([] as ParsedFiles, (set) => {
|
||||
this.getUncommitedFiles().then((files) => {
|
||||
set(files);
|
||||
});
|
||||
|
||||
const unsubscribe = this.listen(set);
|
||||
|
||||
return unsubscribe;
|
||||
});
|
||||
}
|
||||
|
||||
private async getUncommitedFiles() {
|
||||
const uncommitedFiles = await invoke<unknown[]>('get_uncommited_files', {
|
||||
id: this.project.id
|
||||
});
|
||||
|
||||
const orderedFiles = plainToInstance(RemoteFile, uncommitedFiles).sort((a, b) =>
|
||||
a.path?.localeCompare(b.path)
|
||||
);
|
||||
|
||||
return parseRemoteFiles(orderedFiles);
|
||||
}
|
||||
|
||||
private listen(callback: (files: ParsedFiles) => void) {
|
||||
return listen<unknown[]>(`project://${this.project.id}/uncommited-files`, (event) => {
|
||||
const orderedFiles = plainToInstance(RemoteFile, event.payload).sort((a, b) =>
|
||||
a.path?.localeCompare(b.path)
|
||||
);
|
||||
|
||||
callback(parseRemoteFiles(orderedFiles));
|
||||
});
|
||||
}
|
||||
}
|
@ -80,6 +80,10 @@ export class LocalFile {
|
||||
.filter(notNull)
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
get looksConflicted(): boolean {
|
||||
return fileLooksConflicted(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class SkippedFile {
|
||||
@ -292,6 +296,22 @@ export class RemoteFile {
|
||||
get locked(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
get looksConflicted(): boolean {
|
||||
return fileLooksConflicted(this);
|
||||
}
|
||||
}
|
||||
|
||||
function fileLooksConflicted(file: AnyFile) {
|
||||
const hasStartingMarker = file.hunks.some((hunk) =>
|
||||
hunk.diff.split('\n').some((line) => line.startsWith('>>>>>>> theirs', 1))
|
||||
);
|
||||
|
||||
const hasEndingMarker = file.hunks.some((hunk) =>
|
||||
hunk.diff.split('\n').some((line) => line.startsWith('<<<<<<< ours', 1))
|
||||
);
|
||||
|
||||
return hasStartingMarker && hasEndingMarker;
|
||||
}
|
||||
|
||||
export interface Author {
|
||||
|
@ -26,6 +26,7 @@
|
||||
import Navigation from '$lib/navigation/Navigation.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher';
|
||||
import { parseRemoteUrl } from '$lib/url/gitUrl';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
@ -73,6 +74,7 @@
|
||||
setContext(RemoteBranchService, data.remoteBranchService);
|
||||
setContext(BranchListingService, data.branchListingService);
|
||||
setContext(ModeService, data.modeService);
|
||||
setContext(UncommitedFilesWatcher, data.uncommitedFileWatcher);
|
||||
});
|
||||
|
||||
let intervalId: any;
|
||||
|
@ -9,6 +9,7 @@ import { HistoryService } from '$lib/history/history';
|
||||
import { ProjectMetrics } from '$lib/metrics/projectMetrics';
|
||||
import { ModeService } from '$lib/modes/service';
|
||||
import { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
|
||||
import { error } from '@sveltejs/kit';
|
||||
@ -76,6 +77,8 @@ export const load: LayoutLoad = async ({ params, parent }) => {
|
||||
const commitDragActionsFactory = new CommitDragActionsFactory(branchController, project);
|
||||
const reorderDropzoneManagerFactory = new ReorderDropzoneManagerFactory(branchController);
|
||||
|
||||
const uncommitedFileWatcher = new UncommitedFilesWatcher(project);
|
||||
|
||||
return {
|
||||
authService,
|
||||
baseBranchService,
|
||||
@ -93,6 +96,7 @@ export const load: LayoutLoad = async ({ params, parent }) => {
|
||||
branchDragActionsFactory,
|
||||
commitDragActionsFactory,
|
||||
reorderDropzoneManagerFactory,
|
||||
branchListingService
|
||||
branchListingService,
|
||||
uncommitedFileWatcher
|
||||
};
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -17,6 +17,7 @@ use crate::{
|
||||
get_base_branch_data, set_base_branch, set_target_push_remote, update_base_branch,
|
||||
BaseBranch,
|
||||
},
|
||||
branch::get_uncommited_files,
|
||||
branch_manager::BranchManagerExt,
|
||||
file::RemoteBranchFile,
|
||||
remote::{get_branch_data, list_remote_branches, RemoteBranch, RemoteBranchData},
|
||||
@ -539,6 +540,14 @@ impl VirtualBranchActions {
|
||||
.create_virtual_branch_from_branch(branch, remote, guard.write_permission())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_uncommited_files(&self, project: &Project) -> Result<Vec<RemoteBranchFile>> {
|
||||
let context = CommandContext::open(project)?;
|
||||
|
||||
let guard = project.exclusive_worktree_access();
|
||||
|
||||
get_uncommited_files(&context, guard.read_permission())
|
||||
}
|
||||
}
|
||||
|
||||
fn open_with_verify(project: &Project) -> Result<CommandContext> {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::VirtualBranchesExt;
|
||||
use crate::{RemoteBranchFile, VirtualBranchesExt};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use bstr::{BStr, ByteSlice};
|
||||
use core::fmt;
|
||||
@ -6,6 +6,7 @@ use gitbutler_branch::{
|
||||
Branch as GitButlerBranch, BranchId, BranchIdentity, ReferenceExtGix, Target,
|
||||
};
|
||||
use gitbutler_command_context::{CommandContext, GixRepositoryExt};
|
||||
use gitbutler_project::access::WorktreeReadPermission;
|
||||
use gitbutler_reference::normalize_branch_name;
|
||||
use gitbutler_serde::BStringForFrontend;
|
||||
use gix::object::tree::diff::Action;
|
||||
@ -21,6 +22,33 @@ use std::{
|
||||
vec,
|
||||
};
|
||||
|
||||
pub(crate) fn get_uncommited_files(
|
||||
context: &CommandContext,
|
||||
_permission: &WorktreeReadPermission,
|
||||
) -> Result<Vec<RemoteBranchFile>> {
|
||||
let repository = context.repository();
|
||||
let head_commit = repository
|
||||
.head()
|
||||
.context("Failed to get head")?
|
||||
.peel_to_commit()
|
||||
.context("Failed to get head commit")?;
|
||||
|
||||
let files = gitbutler_diff::workdir(repository, &head_commit.id())
|
||||
.context("Failed to list uncommited files")?
|
||||
.into_iter()
|
||||
.map(|(path, file)| {
|
||||
let binary = file.hunks.iter().any(|h| h.binary);
|
||||
RemoteBranchFile {
|
||||
path,
|
||||
hunks: file.hunks,
|
||||
binary,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Returns a list of branches associated with this project.
|
||||
pub fn list_branches(
|
||||
ctx: &CommandContext,
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_branch_actions::RemoteBranchFile;
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_operating_modes::{assure_edit_mode, assure_open_workspace_mode, EditModeMetadata};
|
||||
use gitbutler_oplog::{
|
||||
@ -60,6 +61,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<RemoteBranchFile>> {
|
||||
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();
|
||||
|
@ -4,17 +4,21 @@ 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},
|
||||
@ -24,6 +28,7 @@ use gitbutler_repo::{
|
||||
pub mod commands;
|
||||
|
||||
pub const EDIT_UNCOMMITED_FILES_REF: &str = "refs/gitbutler/edit_uncommited_files";
|
||||
pub const EDIT_INITIAL_STATE_REF: &str = "refs/gitbutler/edit_initial_state";
|
||||
|
||||
fn save_uncommited_files(ctx: &CommandContext) -> Result<()> {
|
||||
let repository = ctx.repository();
|
||||
@ -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,60 @@ 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)?;
|
||||
repository.reference(EDIT_BRANCH_REF, commit_parent.id(), true, "")?;
|
||||
repository.reference(EDIT_INITIAL_STATE_REF, commit_parent.id(), true, "")?;
|
||||
repository.set_head(EDIT_BRANCH_REF)?;
|
||||
repository.checkout_head(Some(CheckoutBuilder::new().force().remove_untracked(true)))?;
|
||||
|
||||
// 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),
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut index = repository.index()?;
|
||||
index.add_all(["*"], git2::IndexAddOption::DEFAULT, None)?;
|
||||
let tree = index.write_tree()?;
|
||||
let tree = repository.find_tree(tree)?;
|
||||
|
||||
let author_signature = signature(SignaturePurpose::Author)?;
|
||||
let committer_signature = signature(SignaturePurpose::Committer)?;
|
||||
// Commit initial state commit
|
||||
repository.commit(
|
||||
Some(EDIT_INITIAL_STATE_REF),
|
||||
&author_signature,
|
||||
&committer_signature,
|
||||
"Initial state commit",
|
||||
&tree,
|
||||
&[&commit_parent],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -331,3 +353,39 @@ pub(crate) fn save_and_return_to_workspace(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn starting_index_state(
|
||||
ctx: &CommandContext,
|
||||
_perm: &WorktreeReadPermission,
|
||||
) -> Result<Vec<RemoteBranchFile>> {
|
||||
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 initial_state = repository
|
||||
.find_reference(EDIT_INITIAL_STATE_REF)?
|
||||
.peel_to_tree()?;
|
||||
|
||||
let diff =
|
||||
repository.diff_tree_to_tree(Some(&commit_parent_tree), Some(&initial_state), None)?;
|
||||
|
||||
let diff_files = hunks_by_filepath(Some(repository), &diff)?
|
||||
.into_iter()
|
||||
.map(|(path, file)| {
|
||||
let binary = file.hunks.iter().any(|h| h.binary);
|
||||
RemoteBranchFile {
|
||||
path,
|
||||
hunks: file.hunks,
|
||||
binary,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(diff_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",
|
||||
}
|
||||
}
|
||||
}
|
@ -17,4 +17,4 @@ pub use config::Config;
|
||||
|
||||
pub mod askpass;
|
||||
|
||||
mod conflicts;
|
||||
pub mod temporary_workdir;
|
||||
|
@ -1,16 +1,15 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use git2::{build::TreeUpdateBuilder, Repository};
|
||||
use git2::build::TreeUpdateBuilder;
|
||||
use gitbutler_cherry_pick::{ConflictedTreeKey, RepositoryExt};
|
||||
use gitbutler_command_context::CommandContext;
|
||||
use gitbutler_commit::{
|
||||
commit_ext::CommitExt,
|
||||
commit_headers::{CommitHeadersV2, HasCommitHeaders},
|
||||
};
|
||||
use gitbutler_error::error::Marker;
|
||||
use tempfile::tempdir;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{conflicts::ConflictedTreeKey, LogUntil, RepoActionsExt, RepositoryExt};
|
||||
use crate::{temporary_workdir::TemporaryWorkdir, 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 +110,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)
|
||||
@ -153,19 +152,10 @@ fn commit_conflicted_cherry_result<'repository>(
|
||||
// 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")?;
|
||||
let temporary_workdir = TemporaryWorkdir::open(repository)?;
|
||||
|
||||
worktree_repository
|
||||
temporary_workdir
|
||||
.repository()
|
||||
.set_index(&mut cherrypick_index)
|
||||
.context("Failed to set cherrypick index as worktree index")?;
|
||||
|
||||
@ -246,28 +236,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")?;
|
||||
|
||||
// Tidy up worktree
|
||||
{
|
||||
temporary_directory.close()?;
|
||||
worktree.prune(None)?;
|
||||
repository
|
||||
.find_branch(&branch_name, git2::BranchType::Local)?
|
||||
.delete()?;
|
||||
}
|
||||
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")?;
|
||||
|
||||
repository
|
||||
.find_commit(commit_oid)
|
||||
|
@ -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.
|
||||
|
69
crates/gitbutler-repo/src/temporary_workdir.rs
Normal file
69
crates/gitbutler-repo/src/temporary_workdir.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tempfile::tempdir;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A temporary workdir created in a temporary directory
|
||||
///
|
||||
/// Gets cleaned up on Drop
|
||||
/// Drop can panic
|
||||
pub struct TemporaryWorkdir {
|
||||
directory: PathBuf,
|
||||
worktree: git2::Worktree,
|
||||
repository: git2::Repository,
|
||||
branch_name: Uuid,
|
||||
cleaned_up: bool,
|
||||
}
|
||||
|
||||
impl TemporaryWorkdir {
|
||||
pub fn open(repository: &git2::Repository) -> Result<Self> {
|
||||
let directory = tempdir().context("Failed to create temporary directory")?;
|
||||
// By using into path, we need to deconstruct the TempDir ourselves
|
||||
let path = directory.into_path();
|
||||
let branch_name = Uuid::new_v4();
|
||||
let worktree = repository
|
||||
.worktree(&branch_name.to_string(), &path.join("repository"), None)
|
||||
.context("Failed to create worktree")?;
|
||||
let worktree_repository = git2::Repository::open_from_worktree(&worktree)
|
||||
.context("Failed to open worktree repository")?;
|
||||
|
||||
Ok(TemporaryWorkdir {
|
||||
repository: worktree_repository,
|
||||
directory: path,
|
||||
worktree,
|
||||
branch_name,
|
||||
cleaned_up: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn repository(&self) -> &git2::Repository {
|
||||
if self.cleaned_up {
|
||||
panic!("Can not access repository after its been closed")
|
||||
}
|
||||
|
||||
&self.repository
|
||||
}
|
||||
|
||||
pub fn close(&mut self) -> Result<()> {
|
||||
if self.cleaned_up {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
std::fs::remove_dir_all(&self.directory)?;
|
||||
self.worktree.prune(None)?;
|
||||
self.repository
|
||||
.find_branch(&self.branch_name.to_string(), git2::BranchType::Local)?
|
||||
.delete()?;
|
||||
|
||||
self.cleaned_up = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TemporaryWorkdir {
|
||||
fn drop(&mut self) {
|
||||
self.close().expect("Failed to close temporary workdir")
|
||||
}
|
||||
}
|
@ -148,6 +148,7 @@ fn main() {
|
||||
repo::commands::git_set_local_config,
|
||||
repo::commands::check_signing_settings,
|
||||
repo::commands::git_clone_repository,
|
||||
repo::commands::get_uncommited_files,
|
||||
virtual_branches::commands::list_virtual_branches,
|
||||
virtual_branches::commands::create_virtual_branch,
|
||||
virtual_branches::commands::delete_local_branch,
|
||||
@ -198,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_branch_actions::RemoteBranchFile;
|
||||
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<RemoteBranchFile>, Error> {
|
||||
let project = projects.get(project_id)?;
|
||||
|
||||
gitbutler_edit_mode::commands::starting_index_state(&project).map_err(Into::into)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub mod commands {
|
||||
use anyhow::{Context, Result};
|
||||
use gitbutler_branch_actions::{RemoteBranchFile, VirtualBranchActions};
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::ProjectId;
|
||||
use gitbutler_repo::RepoCommands;
|
||||
@ -60,4 +61,14 @@ pub mod commands {
|
||||
.context("Failed to checkout main worktree")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn get_uncommited_files(
|
||||
projects: State<'_, projects::Controller>,
|
||||
id: ProjectId,
|
||||
) -> Result<Vec<RemoteBranchFile>, Error> {
|
||||
let project = projects.get(id)?;
|
||||
|
||||
Ok(VirtualBranchActions.get_uncommited_files(&project)?)
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,11 @@ pub(super) mod state {
|
||||
payload: serde_json::json!(virtual_branches),
|
||||
project_id,
|
||||
},
|
||||
Change::UncommitedFiles { project_id, files } => ChangeForFrontend {
|
||||
name: format!("project://{}/uncommited-files", project_id),
|
||||
payload: serde_json::json!(files),
|
||||
project_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{fmt::Display, path::PathBuf};
|
||||
|
||||
use gitbutler_branch_actions::VirtualBranches;
|
||||
use gitbutler_branch_actions::{RemoteBranchFile, VirtualBranches};
|
||||
use gitbutler_operating_modes::OperatingMode;
|
||||
use gitbutler_project::ProjectId;
|
||||
|
||||
@ -101,4 +101,8 @@ pub enum Change {
|
||||
project_id: ProjectId,
|
||||
virtual_branches: VirtualBranches,
|
||||
},
|
||||
UncommitedFiles {
|
||||
project_id: ProjectId,
|
||||
files: Vec<RemoteBranchFile>,
|
||||
},
|
||||
}
|
||||
|
@ -11,8 +11,8 @@ use gitbutler_oplog::{
|
||||
entry::{OperationKind, SnapshotDetails},
|
||||
OplogExt,
|
||||
};
|
||||
use gitbutler_project as projects;
|
||||
use gitbutler_project::ProjectId;
|
||||
use gitbutler_project::{self as projects, Project};
|
||||
use gitbutler_reference::{LocalRefname, Refname};
|
||||
use gitbutler_sync::cloud::{push_oplog, push_repo};
|
||||
use gitbutler_user as users;
|
||||
@ -125,16 +125,29 @@ impl Handler {
|
||||
#[instrument(skip(self, paths, project_id), fields(paths = paths.len()))]
|
||||
fn recalculate_everything(&self, paths: Vec<PathBuf>, project_id: ProjectId) -> Result<()> {
|
||||
let ctx = self.open_command_context(project_id)?;
|
||||
// Skip if we're not on the open workspace mode
|
||||
if !in_open_workspace_mode(&ctx) {
|
||||
return Ok(());
|
||||
|
||||
self.emit_uncommited_files(ctx.project());
|
||||
|
||||
if in_open_workspace_mode(&ctx) {
|
||||
self.maybe_create_snapshot(project_id).ok();
|
||||
self.calculate_virtual_branches(project_id)?;
|
||||
}
|
||||
|
||||
self.maybe_create_snapshot(project_id).ok();
|
||||
self.calculate_virtual_branches(project_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to emit uncommited files. Swollow errors if they arrise.
|
||||
fn emit_uncommited_files(&self, project: &Project) {
|
||||
let Ok(files) = VirtualBranchActions.get_uncommited_files(project) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = self.emit_app_event(Change::UncommitedFiles {
|
||||
project_id: project.id,
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
||||
fn maybe_create_snapshot(&self, project_id: ProjectId) -> anyhow::Result<()> {
|
||||
let project = self
|
||||
.projects
|
||||
|
Loading…
Reference in New Issue
Block a user