+
{@render children()}
diff --git a/apps/desktop/src/lib/utils/getSelectionDirection.ts b/apps/desktop/src/lib/utils/getSelectionDirection.ts
index 9f7fb7baf..339283ef2 100644
--- a/apps/desktop/src/lib/utils/getSelectionDirection.ts
+++ b/apps/desktop/src/lib/utils/getSelectionDirection.ts
@@ -1,4 +1,7 @@
-export function getSelectionDirection(firstFileIndex: number, lastFileIndex: number) {
+export function getSelectionDirection(
+ firstFileIndex: number,
+ lastFileIndex: number
+): 'up' | 'down' {
// detect the direction of the selection
const selectionDirection = lastFileIndex < firstFileIndex ? 'down' : 'up';
diff --git a/apps/desktop/src/lib/utils/selection.ts b/apps/desktop/src/lib/utils/selection.ts
index a30045c2d..f191f4d24 100644
--- a/apps/desktop/src/lib/utils/selection.ts
+++ b/apps/desktop/src/lib/utils/selection.ts
@@ -2,71 +2,96 @@
* Shared helper functions for manipulating selected files with keyboard.
*/
import { getSelectionDirection } from './getSelectionDirection';
+import { KeyName } from './hotkeys';
import { stringifyFileKey, unstringifyFileKey } from '$lib/vbranches/fileIdSelection';
import type { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import type { AnyFile } from '$lib/vbranches/types';
-export function getNextFile(files: AnyFile[], currentId: string): AnyFile | undefined {
+function getFile(files: AnyFile[], id: string): AnyFile | undefined {
+ return files.find((f) => f.id === id);
+}
+
+function getNextFile(files: AnyFile[], currentId: string): AnyFile | undefined {
const fileIndex = files.findIndex((f) => f.id === currentId);
return fileIndex !== -1 && fileIndex + 1 < files.length ? files[fileIndex + 1] : undefined;
}
-export function getPreviousFile(files: AnyFile[], currentId: string): AnyFile | undefined {
+function getPreviousFile(files: AnyFile[], currentId: string): AnyFile | undefined {
const fileIndex = files.findIndex((f) => f.id === currentId);
return fileIndex > 0 ? files[fileIndex - 1] : undefined;
}
-interface MoveSelectionParams {
+function getTopFile(files: AnyFile[], selectedFileIds: string[]): AnyFile | undefined {
+ for (const file of files) {
+ if (selectedFileIds.includes(stringifyFileKey(file.id))) {
+ return file;
+ }
+ }
+ return undefined;
+}
+
+function getBottomFile(files: AnyFile[], selectedFileIds: string[]): AnyFile | undefined {
+ for (let i = files.length - 1; i >= 0; i--) {
+ const file = files[i];
+ if (selectedFileIds.includes(stringifyFileKey(file!.id))) {
+ return file;
+ }
+ }
+ return undefined;
+}
+
+interface UpdateSelectionParams {
allowMultiple: boolean;
shiftKey: boolean;
key: string;
targetElement: HTMLElement;
- file: AnyFile;
files: AnyFile[];
selectedFileIds: string[];
fileIdSelection: FileIdSelection;
commitId?: string;
}
-export function maybeMoveSelection({
+export function updateSelection({
allowMultiple,
shiftKey,
key,
targetElement,
- file,
files,
selectedFileIds,
fileIdSelection,
commitId
-}: MoveSelectionParams) {
+}: UpdateSelectionParams) {
if (!selectedFileIds[0] || selectedFileIds.length === 0) return;
const firstFileId = unstringifyFileKey(selectedFileIds[0]);
const lastFileId = unstringifyFileKey(selectedFileIds.at(-1)!);
+
+ const topFileId = getTopFile(files, selectedFileIds)?.id;
+ const bottomFileId = getBottomFile(files, selectedFileIds)?.id;
+
let selectionDirection = getSelectionDirection(
files.findIndex((f) => f.id === lastFileId),
files.findIndex((f) => f.id === firstFileId)
);
function getAndAddFile(
- getFileFunc: (files: AnyFile[], id: string) => AnyFile | undefined,
- id: string
+ id: string,
+ getFileFunc?: (files: AnyFile[], id: string) => AnyFile | undefined
) {
- const file = getFileFunc(files, id);
+ const file = getFileFunc?.(files, id) ?? getFile(files, id);
if (file) {
// if file is already selected, do nothing
-
if (selectedFileIds.includes(stringifyFileKey(file.id, commitId))) return;
fileIdSelection.add(file.id, commitId);
}
}
- function getAndClearAndAddFile(
- getFileFunc: (files: AnyFile[], id: string) => AnyFile | undefined,
- id: string
+ function getAndClearExcept(
+ id: string,
+ getFileFunc?: (files: AnyFile[], id: string) => AnyFile | undefined
) {
- const file = getFileFunc(files, id);
+ const file = getFileFunc?.(files, id) ?? getFile(files, id);
if (file) {
fileIdSelection.clearExcept(file.id, commitId);
@@ -74,7 +99,7 @@ export function maybeMoveSelection({
}
switch (key) {
- case 'ArrowUp':
+ case KeyName.Up:
if (shiftKey && allowMultiple) {
// Handle case if only one file is selected
// we should update the selection direction
@@ -83,22 +108,21 @@ export function maybeMoveSelection({
} else if (selectionDirection === 'down') {
fileIdSelection.remove(lastFileId, commitId);
}
- getAndAddFile(getPreviousFile, lastFileId);
+ getAndAddFile(lastFileId, getPreviousFile);
} else {
- // focus previous file
- const previousElement = targetElement.previousElementSibling as HTMLElement;
- if (previousElement) previousElement.focus();
-
// Handle reset of selection
- if (selectedFileIds.length > 1) {
- getAndClearAndAddFile(getPreviousFile, lastFileId);
- } else {
- getAndClearAndAddFile(getPreviousFile, file.id);
+ if (selectedFileIds.length > 1 && topFileId !== undefined) {
+ getAndClearExcept(topFileId);
+ }
+
+ // Handle navigation
+ if (selectedFileIds.length === 1) {
+ getAndClearExcept(firstFileId, getPreviousFile);
}
}
break;
- case 'ArrowDown':
+ case KeyName.Down:
if (shiftKey && allowMultiple) {
// Handle case if only one file is selected
// we should update the selection direction
@@ -108,19 +132,22 @@ export function maybeMoveSelection({
fileIdSelection.remove(lastFileId, commitId);
}
- getAndAddFile(getNextFile, lastFileId);
+ getAndAddFile(lastFileId, getNextFile);
} else {
- // focus next file
- const nextElement = targetElement.nextElementSibling as HTMLElement;
- if (nextElement) nextElement.focus();
-
// Handle reset of selection
- if (selectedFileIds.length > 1) {
- getAndClearAndAddFile(getNextFile, lastFileId);
- } else {
- getAndClearAndAddFile(getNextFile, file.id);
+ if (selectedFileIds.length > 1 && bottomFileId !== undefined) {
+ getAndClearExcept(bottomFileId);
+ }
+
+ // Handle navigation
+ if (selectedFileIds.length === 1) {
+ getAndClearExcept(firstFileId, getNextFile);
}
}
break;
+ case KeyName.Escape:
+ fileIdSelection.clear();
+ targetElement.blur();
+ break;
}
}
From a1ff2079db15d952815420cd86bda50b9998ebc0 Mon Sep 17 00:00:00 2001
From: Nico Domino
Date: Wed, 11 Sep 2024 13:43:26 +0200
Subject: [PATCH 12/43] fix: `createPr` arguments order (#4878)
---
.../src/lib/branch/BranchHeader.svelte | 8 ++++++-
.../src/lib/gitHost/github/githubPrService.ts | 21 ++++++++++++-------
.../lib/gitHost/interface/gitHostPrService.ts | 16 +++++++-------
.../src/lib/gitHost/interface/types.ts | 8 +++++++
4 files changed, 36 insertions(+), 17 deletions(-)
diff --git a/apps/desktop/src/lib/branch/BranchHeader.svelte b/apps/desktop/src/lib/branch/BranchHeader.svelte
index 44faa5355..331e8f18b 100644
--- a/apps/desktop/src/lib/branch/BranchHeader.svelte
+++ b/apps/desktop/src/lib/branch/BranchHeader.svelte
@@ -126,7 +126,13 @@
return;
}
- await $prService.createPr(title, body, opts.draft, upstreamBranchName, baseBranchName);
+ await $prService.createPr({
+ title,
+ body,
+ draft: opts.draft,
+ baseBranchName,
+ upstreamName: upstreamBranchName
+ });
} catch (err: any) {
console.error(err);
const toast = mapErrorToToast(err);
diff --git a/apps/desktop/src/lib/gitHost/github/githubPrService.ts b/apps/desktop/src/lib/gitHost/github/githubPrService.ts
index 26c35bb41..892a165b1 100644
--- a/apps/desktop/src/lib/gitHost/github/githubPrService.ts
+++ b/apps/desktop/src/lib/gitHost/github/githubPrService.ts
@@ -8,7 +8,12 @@ import { get, writable } from 'svelte/store';
import type { Persisted } from '$lib/persisted/persisted';
import type { RepoInfo } from '$lib/url/gitUrl';
import type { GitHostPrService } from '../interface/gitHostPrService';
-import type { DetailedPullRequest, MergeMethod, PullRequest } from '../interface/types';
+import type {
+ CreatePullRequestArgs,
+ DetailedPullRequest,
+ MergeMethod,
+ PullRequest
+} from '../interface/types';
import type { Octokit } from '@octokit/rest';
const DEFAULT_PULL_REQUEST_TEMPLATE_PATH = '.github/PULL_REQUEST_TEMPLATE.md';
@@ -23,13 +28,13 @@ export class GitHubPrService implements GitHostPrService {
private pullRequestTemplatePath?: Persisted
) {}
- async createPr(
- title: string,
- body: string,
- draft: boolean,
- baseBranchName: string,
- upstreamName: string
- ): Promise {
+ async createPr({
+ title,
+ body,
+ draft,
+ baseBranchName,
+ upstreamName
+ }: CreatePullRequestArgs): Promise {
this.loading.set(true);
const request = async (pullRequestTemplate: string | undefined = '') => {
const resp = await this.octokit.rest.pulls.create({
diff --git a/apps/desktop/src/lib/gitHost/interface/gitHostPrService.ts b/apps/desktop/src/lib/gitHost/interface/gitHostPrService.ts
index a473ceeae..cc408ffcc 100644
--- a/apps/desktop/src/lib/gitHost/interface/gitHostPrService.ts
+++ b/apps/desktop/src/lib/gitHost/interface/gitHostPrService.ts
@@ -1,6 +1,6 @@
import { buildContextStore } from '$lib/utils/context';
import type { GitHostPrMonitor } from './gitHostPrMonitor';
-import type { DetailedPullRequest, MergeMethod, PullRequest } from './types';
+import type { CreatePullRequestArgs, DetailedPullRequest, MergeMethod, PullRequest } from './types';
import type { Writable } from 'svelte/store';
export const [getGitHostPrService, createGitHostPrServiceStore] = buildContextStore<
@@ -10,13 +10,13 @@ export const [getGitHostPrService, createGitHostPrServiceStore] = buildContextSt
export interface GitHostPrService {
loading: Writable;
get(prNumber: number): Promise;
- createPr(
- title: string,
- body: string,
- draft: boolean,
- baseBranchName: string,
- upstreamName: string
- ): Promise;
+ createPr({
+ title,
+ body,
+ draft,
+ baseBranchName,
+ upstreamName
+ }: CreatePullRequestArgs): Promise;
fetchPrTemplate(path?: string): Promise;
merge(method: MergeMethod, prNumber: number): Promise;
prMonitor(prNumber: number): GitHostPrMonitor;
diff --git a/apps/desktop/src/lib/gitHost/interface/types.ts b/apps/desktop/src/lib/gitHost/interface/types.ts
index 68b5093aa..11bba81f5 100644
--- a/apps/desktop/src/lib/gitHost/interface/types.ts
+++ b/apps/desktop/src/lib/gitHost/interface/types.ts
@@ -76,3 +76,11 @@ export type GitHostArguments = {
baseBranch: string;
forkStr?: string;
};
+
+export type CreatePullRequestArgs = {
+ title: string;
+ body: string;
+ draft: boolean;
+ baseBranchName: string;
+ upstreamName: string;
+};
From 955c99e5d85e7e1c71588e50a22a5a6137681b10 Mon Sep 17 00:00:00 2001
From: Caleb Owens
Date: Wed, 11 Sep 2024 13:43:39 +0200
Subject: [PATCH 13/43] Rebasing working
---
.../src/upstream_integration.rs | 140 +++++++++++-------
.../gitbutler-tauri/src/virtual_branches.rs | 13 +-
2 files changed, 92 insertions(+), 61 deletions(-)
diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs
index 6e3aaf936..c4f0db47e 100644
--- a/crates/gitbutler-branch-actions/src/upstream_integration.rs
+++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs
@@ -1,5 +1,7 @@
use anyhow::{anyhow, bail, Context, Result};
-use gitbutler_branch::{signature, Branch, BranchId, SignaturePurpose, VirtualBranchesHandle};
+use gitbutler_branch::{
+ signature, Branch, BranchId, SignaturePurpose, Target, VirtualBranchesHandle,
+};
use gitbutler_cherry_pick::RepositoryExt as _;
use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
@@ -7,6 +9,7 @@ use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::{
rebase::cherry_rebase_group, LogUntil, RepoActionsExt as _, RepositoryExt as _,
};
+use gix::discover::repository;
use serde::{Deserialize, Serialize};
use crate::{convert_to_real_branch, integration, BranchManagerExt, VirtualBranchesExt as _};
@@ -69,7 +72,7 @@ enum IntegrationResult {
}
pub struct UpstreamIntegrationContext<'a> {
- _perm: Option<&'a mut WorktreeWritePermission>,
+ _permission: Option<&'a mut WorktreeWritePermission>,
repository: &'a git2::Repository,
virtual_branches_in_workspace: Vec,
new_target: git2::Commit<'a>,
@@ -79,7 +82,7 @@ pub struct UpstreamIntegrationContext<'a> {
impl<'a> UpstreamIntegrationContext<'a> {
pub(crate) fn open(
command_context: &'a CommandContext,
- perm: &'a mut WorktreeWritePermission,
+ permission: &'a mut WorktreeWritePermission,
) -> Result {
let virtual_branches_handle = command_context.project().virtual_branches();
let target = virtual_branches_handle.get_default_target()?;
@@ -92,7 +95,7 @@ impl<'a> UpstreamIntegrationContext<'a> {
let virtual_branches_in_workspace = virtual_branches_handle.list_branches_in_workspace()?;
Ok(Self {
- _perm: Some(perm),
+ _permission: Some(permission),
repository,
new_target,
old_target,
@@ -200,56 +203,54 @@ pub(crate) fn integrate_upstream(
resolutions: &[Resolution],
permission: &mut WorktreeWritePermission,
) -> Result<()> {
- let integration_results = {
- let context = UpstreamIntegrationContext::open(command_context, permission)?;
+ let context = UpstreamIntegrationContext::open(command_context, permission)?;
+ let virtual_branches_state = VirtualBranchesHandle::new(command_context.project().gb_dir());
+ let default_target = virtual_branches_state.get_default_target()?;
- // Ensure resolutions match current statuses
- {
- let statuses = upstream_integration_statuses(&context)?;
+ // Ensure resolutions match current statuses
+ {
+ let statuses = upstream_integration_statuses(&context)?;
- let BranchStatuses::UpdatesRequired(statuses) = statuses else {
- bail!("Branches are all up to date")
- };
+ let BranchStatuses::UpdatesRequired(statuses) = statuses else {
+ bail!("Branches are all up to date")
+ };
- if resolutions.len() != context.virtual_branches_in_workspace.len() {
- bail!("Chosen resolutions do not match quantity of applied virtual branches")
- }
-
- let all_resolutions_are_up_to_date = resolutions.iter().all(|resolution| {
- // This is O(n^2), in reality, n is unlikly to be more than 3 or 4
- let Some(branch) = context
- .virtual_branches_in_workspace
- .iter()
- .find(|branch| branch.id == resolution.branch_id)
- else {
- return false;
- };
-
- if resolution.branch_tree != branch.tree {
- return false;
- };
-
- let Some(status) = statuses
- .iter()
- .find(|status| status.0 == resolution.branch_id)
- else {
- return false;
- };
-
- status.1.resolution_acceptable(&resolution.approach)
- });
-
- if !all_resolutions_are_up_to_date {
- bail!("Chosen resolutions do not match current integration statuses")
- }
+ if resolutions.len() != context.virtual_branches_in_workspace.len() {
+ bail!("Chosen resolutions do not match quantity of applied virtual branches")
}
- compute_resolutions(&context, resolutions)?
- };
+ let all_resolutions_are_up_to_date = resolutions.iter().all(|resolution| {
+ // This is O(n^2), in reality, n is unlikly to be more than 3 or 4
+ let Some(branch) = context
+ .virtual_branches_in_workspace
+ .iter()
+ .find(|branch| branch.id == resolution.branch_id)
+ else {
+ return false;
+ };
+
+ if resolution.branch_tree != branch.tree {
+ return false;
+ };
+
+ let Some(status) = statuses
+ .iter()
+ .find(|status| status.0 == resolution.branch_id)
+ else {
+ return false;
+ };
+
+ status.1.resolution_acceptable(&resolution.approach)
+ });
+
+ if !all_resolutions_are_up_to_date {
+ bail!("Chosen resolutions do not match current integration statuses")
+ }
+ }
+
+ let integration_results = compute_resolutions(&context, resolutions)?;
{
- let virtual_branches_state = VirtualBranchesHandle::new(command_context.project().gb_dir());
-
// We preform the updates in stages. If deleting or unapplying fails, we
// could enter a much worse state if we're simultaniously updating trees
@@ -264,6 +265,8 @@ pub(crate) fn integrate_upstream(
command_context.delete_branch_reference(&branch)?;
}
+ let permission = context._permission.expect("Permission provided above");
+
// Unapply branches
for (branch_id, integration_result) in &integration_results {
if !matches!(integration_result, IntegrationResult::UnapplyBranch) {
@@ -277,6 +280,10 @@ pub(crate) fn integrate_upstream(
let mut branches = virtual_branches_state.list_branches_in_workspace()?;
+ let new_target_tree = context.new_target.tree()?;
+ let mut final_tree = context.new_target.tree()?;
+ let repository = context.repository;
+
// Update branch trees
for (branch_id, integration_result) in &integration_results {
let IntegrationResult::UpdatedObjects { head, tree } = integration_result else {
@@ -291,7 +298,28 @@ pub(crate) fn integrate_upstream(
branch.tree = *tree;
virtual_branches_state.set_branch(branch.clone())?;
+
+ // Combine tree into new working tree
+ {
+ let branch_tree = repository.find_tree(branch.tree)?;
+ let mut merge_result: git2::Index =
+ repository.merge_trees(&new_target_tree, &final_tree, &branch_tree, None)?;
+ let final_tree_oid = merge_result.write_tree_to(repository)?;
+ final_tree = repository.find_tree(final_tree_oid)?;
+ }
}
+
+ repository.checkout_tree_builder(&final_tree)
+ .force()
+ .checkout()
+ .context("failed to checkout index, this should not have happened, we should have already detected this")?;
+
+ virtual_branches_state.set_default_target(Target {
+ sha: context.new_target.id(),
+ ..default_target
+ })?;
+
+ crate::integration::update_workspace_commit(&virtual_branches_state, command_context)?;
}
Ok(())
@@ -481,7 +509,7 @@ mod test {
let head_commit = commit_file(&repository, Some(&initial_commit), &[("foo.txt", "baz")]);
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target: head_commit.clone(),
new_target: head_commit,
repository: &repository,
@@ -503,7 +531,7 @@ mod test {
let new_target = commit_file(&repository, Some(&old_target), &[("foo.txt", "qux")]);
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target,
repository: &repository,
@@ -527,7 +555,7 @@ mod test {
let branch = make_branch(old_target.id(), old_target.tree_id());
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target,
repository: &repository,
@@ -564,7 +592,7 @@ mod test {
let branch = make_branch(branch_head.id(), branch_head.tree_id());
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target: new_target.clone(),
repository: &repository,
@@ -626,7 +654,7 @@ mod test {
let branch = make_branch(old_target.id(), branch_head.tree_id());
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target,
repository: &repository,
@@ -657,7 +685,7 @@ mod test {
let branch = make_branch(branch_head.id(), branch_tree.tree_id());
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target,
repository: &repository,
@@ -686,7 +714,7 @@ mod test {
let branch = make_branch(new_target.id(), new_target.tree_id());
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target,
repository: &repository,
@@ -724,7 +752,7 @@ mod test {
let branch = make_branch(new_target.id(), tree.tree_id());
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target,
repository: &repository,
@@ -771,7 +799,7 @@ mod test {
let branch = make_branch(branch_head.id(), branch_tree.tree_id());
let context = UpstreamIntegrationContext {
- _perm: None,
+ _permission: None,
old_target,
new_target,
repository: &repository,
diff --git a/crates/gitbutler-tauri/src/virtual_branches.rs b/crates/gitbutler-tauri/src/virtual_branches.rs
index 4e69b897d..d1646c059 100644
--- a/crates/gitbutler-tauri/src/virtual_branches.rs
+++ b/crates/gitbutler-tauri/src/virtual_branches.rs
@@ -609,17 +609,20 @@ pub mod commands {
}
#[tauri::command(async)]
- #[instrument(skip(projects), err(Debug))]
+ #[instrument(skip(projects, windows), err(Debug))]
pub fn integrate_upstream(
+ windows: State<'_, WindowState>,
projects: State<'_, projects::Controller>,
project_id: ProjectId,
resolutions: Vec,
) -> Result<(), Error> {
let project = projects.get(project_id)?;
- Ok(gitbutler_branch_actions::integrate_upstream(
- &project,
- &resolutions,
- )?)
+
+ gitbutler_branch_actions::integrate_upstream(&project, &resolutions)?;
+
+ emit_vbranches(&windows, project_id);
+
+ Ok(())
}
fn emit_vbranches(windows: &WindowState, project_id: projects::ProjectId) {
From 05953194054b23ffe2b44d685ca4f83868c46ded Mon Sep 17 00:00:00 2001
From: Nico Domino
Date: Wed, 11 Sep 2024 13:59:08 +0200
Subject: [PATCH 14/43] fix: rm unnecessary `Space.svelte` and add
`List`/`ListItem` (#4874)
---
.../src/lib/components/MarkdownContent.svelte | 35 ++++++++++++++-----
.../components/markdownRenderers/List.svelte | 21 +++++++++++
.../markdownRenderers/ListItem.svelte | 13 +++++++
.../components/markdownRenderers/Space.svelte | 2 --
.../src/lib/utils/markdownRenderers.ts | 6 ++--
5 files changed, 64 insertions(+), 13 deletions(-)
create mode 100644 apps/desktop/src/lib/components/markdownRenderers/List.svelte
create mode 100644 apps/desktop/src/lib/components/markdownRenderers/ListItem.svelte
delete mode 100644 apps/desktop/src/lib/components/markdownRenderers/Space.svelte
diff --git a/apps/desktop/src/lib/components/MarkdownContent.svelte b/apps/desktop/src/lib/components/MarkdownContent.svelte
index 669881ce5..d207044db 100644
--- a/apps/desktop/src/lib/components/MarkdownContent.svelte
+++ b/apps/desktop/src/lib/components/MarkdownContent.svelte
@@ -8,25 +8,42 @@
| Tokens.Link
| Tokens.Heading
| Tokens.Image
- | Tokens.Space
| Tokens.Blockquote
| Tokens.Code
+ | Tokens.Text
| Tokens.Codespan
- | Tokens.Text;
+ | Tokens.Paragraph
+ | Tokens.ListItem
+ | Tokens.List;
let { type, ...rest }: Props = $props();
+
+ // @ts-expect-error indexing on string union is having trouble
+ const CurrentComponent = renderers[type as Props['type']];
-{#if type && renderers[type as keyof typeof renderers]}
-
- {#if 'tokens' in rest}
-
- {/if}
-
+{#if type && CurrentComponent}
+ {#if type === 'list'}
+ {@const listItems = (rest as Extract).items}
+
+ {#each listItems as item}
+ {@const ChildComponent = renderers[item.type]}
+
+
+
+ {/each}
+
+ {:else}
+
+ {#if 'tokens' in rest}
+
+ {/if}
+
+ {/if}
{:else if 'tokens' in rest && rest.tokens}
{#each rest.tokens as token}
{/each}
{:else if 'raw' in rest}
- {@html rest.raw?.replaceAll('\n', '
') ?? ''}
+ {@html rest.raw?.replaceAll('\n', '') ?? ''}
{/if}
diff --git a/apps/desktop/src/lib/components/markdownRenderers/List.svelte b/apps/desktop/src/lib/components/markdownRenderers/List.svelte
new file mode 100644
index 000000000..5e15d9492
--- /dev/null
+++ b/apps/desktop/src/lib/components/markdownRenderers/List.svelte
@@ -0,0 +1,21 @@
+
+
+{#if ordered}
+
+ {@render children()}
+
+{:else}
+
+{/if}
diff --git a/apps/desktop/src/lib/components/markdownRenderers/ListItem.svelte b/apps/desktop/src/lib/components/markdownRenderers/ListItem.svelte
new file mode 100644
index 000000000..75789e135
--- /dev/null
+++ b/apps/desktop/src/lib/components/markdownRenderers/ListItem.svelte
@@ -0,0 +1,13 @@
+
+
+
+ {@render children()}
+
diff --git a/apps/desktop/src/lib/components/markdownRenderers/Space.svelte b/apps/desktop/src/lib/components/markdownRenderers/Space.svelte
deleted file mode 100644
index 17c760423..000000000
--- a/apps/desktop/src/lib/components/markdownRenderers/Space.svelte
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/apps/desktop/src/lib/utils/markdownRenderers.ts b/apps/desktop/src/lib/utils/markdownRenderers.ts
index a95394240..8a92c5024 100644
--- a/apps/desktop/src/lib/utils/markdownRenderers.ts
+++ b/apps/desktop/src/lib/utils/markdownRenderers.ts
@@ -3,19 +3,21 @@ import Code from '$lib/components/markdownRenderers/Code.svelte';
import Codespan from '$lib/components/markdownRenderers/Codespan.svelte';
import Heading from '$lib/components/markdownRenderers/Heading.svelte';
import Image from '$lib/components/markdownRenderers/Image.svelte';
+import List from '$lib/components/markdownRenderers/List.svelte';
+import ListItem from '$lib/components/markdownRenderers/ListItem.svelte';
import Paragraph from '$lib/components/markdownRenderers/Paragraph.svelte';
-import Space from '$lib/components/markdownRenderers/Space.svelte';
import Text from '$lib/components/markdownRenderers/Text.svelte';
import Link from '$lib/shared/Link.svelte';
export const renderers = {
link: Link,
image: Image,
- space: Space,
blockquote: Blockquote,
code: Code,
codespan: Codespan,
text: Text,
+ list: List,
+ list_item: ListItem,
heading: Heading,
paragraph: Paragraph
};
From 6e7aefd5c31dabdf61d27c607e8a5db56614610f Mon Sep 17 00:00:00 2001
From: Caleb Owens
Date: Wed, 11 Sep 2024 14:47:36 +0200
Subject: [PATCH 15/43] Merge commiting
---
.../gitbutler-branch-actions/src/actions.rs | 1 -
.../src/upstream_integration.rs | 77 ++++++--
crates/gitbutler-edit-mode/src/lib.rs | 5 +-
crates/gitbutler-repo/src/rebase.rs | 168 ++++++++++++++----
crates/gitbutler-repo/src/repository.rs | 34 +---
crates/gitbutler-repo/src/repository_ext.rs | 28 ++-
6 files changed, 231 insertions(+), 82 deletions(-)
diff --git a/crates/gitbutler-branch-actions/src/actions.rs b/crates/gitbutler-branch-actions/src/actions.rs
index 8b1c6ad7d..97a24a3c7 100644
--- a/crates/gitbutler-branch-actions/src/actions.rs
+++ b/crates/gitbutler-branch-actions/src/actions.rs
@@ -1,5 +1,4 @@
use super::r#virtual as vbranch;
-use super::r#virtual as branch;
use crate::upstream_integration::{self, BranchStatuses, Resolution, UpstreamIntegrationContext};
use crate::{
base,
diff --git a/crates/gitbutler-branch-actions/src/upstream_integration.rs b/crates/gitbutler-branch-actions/src/upstream_integration.rs
index c4f0db47e..9a60c8ecf 100644
--- a/crates/gitbutler-branch-actions/src/upstream_integration.rs
+++ b/crates/gitbutler-branch-actions/src/upstream_integration.rs
@@ -7,12 +7,12 @@ use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_project::access::WorktreeWritePermission;
use gitbutler_repo::{
- rebase::cherry_rebase_group, LogUntil, RepoActionsExt as _, RepositoryExt as _,
+ rebase::{cherry_rebase_group, gitbutler_merge_commits},
+ LogUntil, RepoActionsExt as _, RepositoryExt as _,
};
-use gix::discover::repository;
use serde::{Deserialize, Serialize};
-use crate::{convert_to_real_branch, integration, BranchManagerExt, VirtualBranchesExt as _};
+use crate::{BranchManagerExt, VirtualBranchesExt as _};
#[derive(Serialize, PartialEq, Debug)]
#[serde(tag = "type", content = "subject", rename_all = "camelCase")]
@@ -77,6 +77,7 @@ pub struct UpstreamIntegrationContext<'a> {
virtual_branches_in_workspace: Vec,
new_target: git2::Commit<'a>,
old_target: git2::Commit<'a>,
+ target_branch_name: String,
}
impl<'a> UpstreamIntegrationContext<'a> {
@@ -100,6 +101,7 @@ impl<'a> UpstreamIntegrationContext<'a> {
new_target,
old_target,
virtual_branches_in_workspace,
+ target_branch_name: target.branch.branch().to_string(),
})
}
}
@@ -332,8 +334,8 @@ fn compute_resolutions(
let UpstreamIntegrationContext {
repository,
new_target,
- old_target,
virtual_branches_in_workspace,
+ target_branch_name,
..
} = context;
@@ -358,15 +360,57 @@ fn compute_resolutions(
// Make a merge commit on top of the branch commits,
// then rebase the tree ontop of that. If the tree ends
// up conflicted, commit the tree.
- todo!();
+ let target_commit = repository.find_commit(virtual_branch.head)?;
- Ok((
- virtual_branch.id,
- IntegrationResult::UpdatedObjects {
- head: todo!(),
- tree: todo!(),
- },
- ))
+ let new_head = gitbutler_merge_commits(
+ repository,
+ target_commit,
+ new_target.clone(),
+ &virtual_branch.name,
+ target_branch_name,
+ )?;
+
+ let head = repository.find_commit(virtual_branch.head)?;
+ let tree = repository.find_tree(virtual_branch.tree)?;
+
+ // Rebase tree
+ let author_signature = signature(SignaturePurpose::Author)
+ .context("Failed to get gitbutler signature")?;
+ let committer_signature = signature(SignaturePurpose::Committer)
+ .context("Failed to get gitbutler signature")?;
+ let committed_tree = repository.commit(
+ None,
+ &author_signature,
+ &committer_signature,
+ "Uncommited changes",
+ &tree,
+ &[&head],
+ )?;
+
+ // Rebase commited tree
+ let new_commited_tree =
+ cherry_rebase_group(repository, new_head.id(), &[committed_tree], true)?;
+ let new_commited_tree = repository.find_commit(new_commited_tree)?;
+
+ if new_commited_tree.is_conflicted() {
+ Ok((
+ virtual_branch.id,
+ IntegrationResult::UpdatedObjects {
+ head: new_commited_tree.id(),
+ tree: repository
+ .find_real_tree(&new_commited_tree, Default::default())?
+ .id(),
+ },
+ ))
+ } else {
+ Ok((
+ virtual_branch.id,
+ IntegrationResult::UpdatedObjects {
+ head: new_head.id(),
+ tree: new_commited_tree.tree_id(),
+ },
+ ))
+ }
}
ResolutionApproach::Rebase => {
// Rebase the commits, then try rebasing the tree. If
@@ -514,6 +558,7 @@ mod test {
new_target: head_commit,
repository: &repository,
virtual_branches_in_workspace: vec![],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -536,6 +581,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -560,6 +606,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -597,6 +644,7 @@ mod test {
new_target: new_target.clone(),
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -659,6 +707,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -690,6 +739,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -719,6 +769,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -757,6 +808,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
@@ -804,6 +856,7 @@ mod test {
new_target,
repository: &repository,
virtual_branches_in_workspace: vec![branch.clone()],
+ target_branch_name: "main".to_string(),
};
assert_eq!(
diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs
index fef750e46..cdd00fa31 100644
--- a/crates/gitbutler-edit-mode/src/lib.rs
+++ b/crates/gitbutler-edit-mode/src/lib.rs
@@ -236,7 +236,6 @@ pub(crate) fn save_and_return_to_workspace(
let commit = repository
.find_commit(edit_mode_metadata.commit_oid)
.context("Failed to find commit")?;
- let commit_parent = commit.parent(0).context("Failed to get commit's parent")?;
let stashed_workspace_changes_reference = repository
.find_reference(EDIT_UNCOMMITED_FILES_REF)
.context("Failed to find stashed workspace changes")?;
@@ -250,6 +249,8 @@ pub(crate) fn save_and_return_to_workspace(
bail!("Failed to find virtual branch for this reference. Entering and leaving edit mode for non-virtual branches is unsupported")
};
+ let parents = commit.parents().collect::>();
+
// Recommit commit
let tree = repository.create_wd_tree()?;
let commit_headers = commit
@@ -266,7 +267,7 @@ pub(crate) fn save_and_return_to_workspace(
&commit.committer(),
&commit.message_bstr().to_str_lossy(),
&tree,
- &[&commit_parent],
+ &parents.iter().collect::>(),
commit_headers,
)
.context("Failed to commit new commit")?;
diff --git a/crates/gitbutler-repo/src/rebase.rs b/crates/gitbutler-repo/src/rebase.rs
index 4b993d873..124d28e48 100644
--- a/crates/gitbutler-repo/src/rebase.rs
+++ b/crates/gitbutler-repo/src/rebase.rs
@@ -7,7 +7,6 @@ use gitbutler_commit::{
commit_headers::{CommitHeadersV2, HasCommitHeaders},
};
use gitbutler_error::error::Marker;
-use itertools::Itertools;
use crate::{LogUntil, RepositoryExt as _};
@@ -80,9 +79,21 @@ pub fn cherry_rebase_group(
if !succeeding_rebases {
return Err(anyhow!("failed to rebase")).context(Marker::BranchConflict);
}
- commit_conflicted_cherry_result(repository, head, to_rebase, cherrypick_index)
+ commit_conflicted_cherry_result(
+ repository,
+ head,
+ to_rebase,
+ cherrypick_index,
+ None,
+ )
} else {
- commit_unconflicted_cherry_result(repository, head, to_rebase, cherrypick_index)
+ commit_unconflicted_cherry_result(
+ repository,
+ head,
+ to_rebase,
+ cherrypick_index,
+ None,
+ )
}
},
)?
@@ -91,11 +102,19 @@ pub fn cherry_rebase_group(
Ok(new_head_id)
}
+pub struct OverrideCommitDetails<'a, 'repository> {
+ message: &'a str,
+ parents: &'a [&'a git2::Commit<'repository>],
+ author: &'a git2::Signature<'repository>,
+ commiter: &'a git2::Signature<'repository>,
+}
+
fn commit_unconflicted_cherry_result<'repository>(
repository: &'repository git2::Repository,
head: git2::Commit<'repository>,
to_rebase: git2::Commit,
mut cherrypick_index: git2::Index,
+ override_commit_details: Option,
) -> Result> {
let commit_headers = to_rebase.gitbutler_headers();
@@ -119,17 +138,31 @@ fn commit_unconflicted_cherry_result<'repository>(
..commit_headers
});
- 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")?;
+ let commit_oid = if let Some(override_commit_details) = override_commit_details {
+ crate::RepositoryExt::commit_with_signature(
+ repository,
+ None,
+ override_commit_details.author,
+ override_commit_details.commiter,
+ override_commit_details.message,
+ &merge_tree,
+ override_commit_details.parents,
+ commit_headers,
+ )
+ .context("failed to create commit")?
+ } else {
+ 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)
@@ -141,6 +174,7 @@ fn commit_conflicted_cherry_result<'repository>(
head: git2::Commit,
to_rebase: git2::Commit,
cherrypick_index: git2::Index,
+ override_commit_details: Option,
) -> Result> {
let commit_headers = to_rebase.gitbutler_headers();
@@ -206,33 +240,91 @@ fn commit_conflicted_cherry_result<'repository>(
let tree_oid = tree_writer.write().context("failed to write tree")?;
- let commit_headers = commit_headers.map(|commit_headers| {
- let conflicted_file_count = dbg!(conflicted_files)
- .len()
- .try_into()
- .expect("If you have more than 2^64 conflicting files, we've got bigger problems");
- CommitHeadersV2 {
- conflicted: Some(conflicted_file_count),
- ..commit_headers
- }
- });
+ let commit_headers =
+ commit_headers
+ .or_else(|| Some(Default::default()))
+ .map(|commit_headers| {
+ let conflicted_file_count = conflicted_files.len().try_into().expect(
+ "If you have more than 2^64 conflicting files, we've got bigger problems",
+ );
+ CommitHeadersV2 {
+ conflicted: Some(conflicted_file_count),
+ ..commit_headers
+ }
+ });
- // write a commit
- let commit_oid = 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")?;
+ let commit_oid = if let Some(override_commit_details) = override_commit_details {
+ crate::RepositoryExt::commit_with_signature(
+ repository,
+ None,
+ override_commit_details.author,
+ override_commit_details.commiter,
+ override_commit_details.message,
+ &repository
+ .find_tree(tree_oid)
+ .context("failed to find tree")?,
+ override_commit_details.parents,
+ commit_headers,
+ )
+ .context("failed to create commit")?
+ } else {
+ 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)
.context("failed to find commit")
}
+
+pub fn gitbutler_merge_commits<'repository>(
+ repository: &'repository git2::Repository,
+ target_commit: git2::Commit<'repository>,
+ incoming_commit: git2::Commit<'repository>,
+ target_branch_name: &str,
+ incoming_branch_name: &str,
+) -> Result> {
+ let cherrypick_index =
+ repository.cherry_pick_gitbutler(&target_commit, &incoming_commit, None)?;
+
+ let (author, committer) = repository.signatures()?;
+
+ let override_commit_details = OverrideCommitDetails {
+ message: &format!(
+ "Merge branch `{}` into `{}`",
+ incoming_branch_name, target_branch_name
+ ),
+ parents: &[&target_commit.clone(), &incoming_commit.clone()],
+ author: &author,
+ commiter: &committer,
+ };
+
+ if cherrypick_index.has_conflicts() {
+ commit_conflicted_cherry_result(
+ repository,
+ target_commit,
+ incoming_commit,
+ cherrypick_index,
+ Some(override_commit_details),
+ )
+ } else {
+ commit_unconflicted_cherry_result(
+ repository,
+ target_commit,
+ incoming_commit,
+ cherrypick_index,
+ Some(override_commit_details),
+ )
+ }
+}
diff --git a/crates/gitbutler-repo/src/repository.rs b/crates/gitbutler-repo/src/repository.rs
index ea3d33802..1469e8297 100644
--- a/crates/gitbutler-repo/src/repository.rs
+++ b/crates/gitbutler-repo/src/repository.rs
@@ -1,14 +1,14 @@
use std::str::FromStr;
use anyhow::{anyhow, Context, Result};
-use gitbutler_branch::{gix_to_git2_signature, Branch, BranchId, SignaturePurpose};
+use gitbutler_branch::{Branch, BranchId};
use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_headers::CommitHeadersV2;
use gitbutler_error::error::Code;
use gitbutler_project::AuthKey;
use gitbutler_reference::{Refname, RemoteRefname};
-use crate::{askpass, credentials, Config, RepositoryExt};
+use crate::{askpass, credentials, RepositoryExt};
pub trait RepoActionsExt {
fn fetch(&self, remote_name: &str, askpass: Option) -> Result<()>;
fn push(
@@ -35,7 +35,6 @@ pub trait RepoActionsExt {
branch_name: &str,
askpass: Option