From b7c38e4f33fb5dc2b9fdc50ca0dc149efd63465a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Aug 2024 09:07:33 +0200 Subject: [PATCH 1/6] fix assumption about branch identity It's possible for virtual branches to have the same 'identity' as the target branch, yet they should be listed. --- crates/gitbutler-branch-actions/src/branch.rs | 11 ++++++++--- .../tests/virtual_branches/list.rs | 5 ++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/gitbutler-branch-actions/src/branch.rs b/crates/gitbutler-branch-actions/src/branch.rs index 86442da3a..10fa72da3 100644 --- a/crates/gitbutler-branch-actions/src/branch.rs +++ b/crates/gitbutler-branch-actions/src/branch.rs @@ -88,7 +88,7 @@ fn combine_branches( continue; }; // Skip branches that should not be listed, e.g. the target 'main' or the gitbutler technical branches like 'gitbutler/integration' - if !should_list_git_branch(&identity, &target_branch) { + if !should_list_git_branch(&identity, &target_branch, &branch) { continue; } groups.entry(identity).or_default().push(branch); @@ -255,12 +255,17 @@ impl GroupBranch<'_> { } } } + + /// Returns true if this is a virtual branch. + fn is_virtual(&self) -> bool { + matches!(self, Self::Virtual(_)) + } } /// Determines if a branch should be listed in the UI. /// This excludes the target branch as well as gitbutler specific branches. -fn should_list_git_branch(identity: &str, target: &Target) -> bool { - if identity == target.branch.branch() { +fn should_list_git_branch(identity: &str, target: &Target, branch: &GroupBranch) -> bool { + if !branch.is_virtual() && identity == target.branch.branch() { return false; } // Exclude gitbutler technical branches (not useful for the user) diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs index 14c829bc7..ca90e724c 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs @@ -116,9 +116,8 @@ fn one_feature_branch_and_one_vbranch_on_integration_one_commit() -> Result<()> let list = list_branches(&ctx, None)?; assert_eq!( list.len(), - 0, - "Strange, one is definitely there and it seems valid to name vbranches\ - after the target branch but it's filtered out here" + 1, + "it finds our single virtual branch despit it having the same 'identity' as the target branch: 'main'" ); Ok(()) From e6b00b5e0ea86d6279d2859ed420a3dc042f1aa9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Aug 2024 09:24:05 +0200 Subject: [PATCH 2/6] more tests and refactors for branch-listing --- crates/gitbutler-branch-actions/src/branch.rs | 13 +- .../tests/virtual_branches/list.rs | 235 +++++++++++------- crates/gitbutler-commit/src/commit_headers.rs | 3 + 3 files changed, 161 insertions(+), 90 deletions(-) diff --git a/crates/gitbutler-branch-actions/src/branch.rs b/crates/gitbutler-branch-actions/src/branch.rs index 10fa72da3..0a278a038 100644 --- a/crates/gitbutler-branch-actions/src/branch.rs +++ b/crates/gitbutler-branch-actions/src/branch.rs @@ -299,10 +299,10 @@ pub struct BranchListingFilter { #[derive(Debug, Clone, Serialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BranchListing { - /// The name of the branch (e.g. `main`, `feature/branch`), excluding the remote name + /// The `identity` of the branch (e.g. `main`, `feature/branch`), excluding the remote name. pub name: String, - /// This is a list of remote that this branch can be found on (e.g. `origin`, `upstream` etc.). - /// If this branch is a local branch, this list will be empty. + /// This is a list of remotes that this branch can be found on (e.g. `origin`, `upstream` etc.), + /// by collecting remotes from all local branches with the same identity that have a tracking setup. #[serde(serialize_with = "gitbutler_serde::serde::as_string_lossy_vec")] pub remotes: Vec, /// The branch may or may not have a virtual branch associated with it @@ -316,7 +316,8 @@ pub struct BranchListing { /// This includes any commits, uncommited changes or even updates to the branch metadata (e.g. renaming). pub updated_at: u128, /// A list of authors that have contributes commits to this branch. - /// In the case of multiple remote tracking branches, it takes the full list of unique authors. + /// In the case of multiple remote tracking branches, or branches whose commits are evaluated, + /// it takes the full list of unique authors, without applying a mailmap. pub authors: Vec, /// Determines if the current user is involved with this branch. /// Returns true if the author has created a commit on this branch @@ -328,10 +329,10 @@ pub struct BranchListing { /// 2. The head of the local branch /// 3. The head of the first remote branch #[serde(skip)] - head: git2::Oid, + pub head: git2::Oid, } -/// Represents a "commit author" or "signature", based on the data from ther git history +/// Represents a "commit author" or "signature", based on the data from the git history #[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)] pub struct Author { /// The name of the author as configured in the git config diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs index ca90e724c..78197e4a9 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs @@ -1,6 +1,5 @@ use anyhow::Result; -use gitbutler_branch_actions::{list_branches, Author, BranchListingFilter}; -use gitbutler_command_context::CommandContext; +use gitbutler_branch_actions::{list_branches, BranchListingFilter}; #[test] fn one_vbranch_on_integration() -> Result<()> { @@ -8,19 +7,16 @@ fn one_vbranch_on_integration() -> Result<()> { let list = list_branches(&project_ctx("one-vbranch-on-integration")?, None)?; assert_eq!(list.len(), 1); - let branch = &list[0]; - assert_eq!(branch.name, "virtual"); - assert!(branch.remotes.is_empty(), "no remote is associated yet"); - assert_eq!(branch.number_of_commits, 0); - assert_eq!( - branch - .virtual_branch - .as_ref() - .map(|v| v.given_name.as_str()), - Some("virtual") + assert_equal( + &list[0], + ExpectedBranchListing { + identity: "virtual", + virtual_branch_given_name: Some("virtual"), + virtual_branch_in_workspace: true, + ..Default::default() + }, + "It's a bare virtual branch with no commit", ); - assert_eq!(branch.authors, []); - assert!(branch.own_branch, "zero commits means user owns the branch"); Ok(()) } @@ -31,19 +27,17 @@ fn one_vbranch_on_integration_one_commit() -> Result<()> { let list = list_branches(&ctx, None)?; assert_eq!(list.len(), 1); - let branch = &list[0]; - assert_eq!(branch.name, "virtual"); - assert!(branch.remotes.is_empty(), "no remote is associated yet"); - assert_eq!( - branch - .virtual_branch - .as_ref() - .map(|v| v.given_name.as_str()), - Some("virtual") + assert_equal( + &list[0], + ExpectedBranchListing { + identity: "virtual", + virtual_branch_given_name: Some("virtual"), + virtual_branch_in_workspace: true, + number_of_commits: 1, + ..Default::default() + }, + "It's a bare virtual branch with a single commit", ); - assert_eq!(branch.number_of_commits, 1, "one commit created on vbranch"); - assert_eq!(branch.authors, [default_author()]); - assert!(branch.own_branch); Ok(()) } @@ -51,9 +45,6 @@ fn one_vbranch_on_integration_one_commit() -> Result<()> { fn two_vbranches_on_integration_one_commit() -> Result<()> { init_env(); let ctx = project_ctx("two-vbranches-on-integration-one-applied")?; - // let list = list_branches(&ctx, None)?; - // assert_eq!(list.len(), 2, "all branches are listed"); - let list = list_branches( &ctx, Some(BranchListingFilter { @@ -62,24 +53,15 @@ fn two_vbranches_on_integration_one_commit() -> Result<()> { }), )?; assert_eq!(list.len(), 1, "only one of these is applied"); - let branch = &list[0]; - assert_eq!(branch.name, "other"); - assert!(branch.remotes.is_empty(), "no remote is associated yet"); - assert_eq!( - branch - .virtual_branch - .as_ref() - .map(|v| v.given_name.as_str()), - Some("other") - ); - assert_eq!( - branch.number_of_commits, 0, - "this one has only pending changes in the worktree" - ); - assert_eq!(branch.authors, []); - assert!( - branch.own_branch, - "empty branches are always considered owned (or something the user is involved in)" + assert_equal( + &list[0], + ExpectedBranchListing { + identity: "other", + virtual_branch_given_name: Some("other"), + virtual_branch_in_workspace: true, + ..Default::default() + }, + "It's a bare virtual branch without any branches with the same identity", ); let list = list_branches( @@ -90,21 +72,16 @@ fn two_vbranches_on_integration_one_commit() -> Result<()> { }), )?; assert_eq!(list.len(), 1, "only one of these is *not* applied"); - let branch = &list[0]; - assert_eq!(branch.name, "virtual"); - assert!(branch.remotes.is_empty(), "no remote is associated yet"); - assert_eq!( - branch - .virtual_branch - .as_ref() - .map(|v| v.given_name.as_str()), - Some("virtual") - ); - assert_eq!(branch.number_of_commits, 1, "here we have a commit"); - assert_eq!(branch.authors, [default_author()]); - assert!( - branch.own_branch, - "the current user (as identified by signature) created the commit" + assert_equal( + &list[0], + ExpectedBranchListing { + identity: "virtual", + virtual_branch_given_name: Some("virtual"), + virtual_branch_in_workspace: false, + number_of_commits: 1, + ..Default::default() + }, + "It's a bare virtual branch without any branches with the same identity", ); Ok(()) } @@ -119,34 +96,124 @@ fn one_feature_branch_and_one_vbranch_on_integration_one_commit() -> Result<()> 1, "it finds our single virtual branch despit it having the same 'identity' as the target branch: 'main'" ); + assert_equal( + &list[0], + ExpectedBranchListing { + identity: "main", + remotes: vec![], + // remotes: vec!["origin"], // TODO: should be this + virtual_branch_given_name: Some("main"), + virtual_branch_in_workspace: true, + number_of_commits: 1, + ..Default::default() + }, + "virtual branches can have the name of the target, even though it's probably not going to work when pushing. \ + The remotes of the local `refs/heads/main` are shown." + ); Ok(()) } -/// This function affects all tests, but those who care should just call it, assuming -/// they all care for the same default value. -/// If not, they should be placed in their own integration test or run with `#[serial_test:serial]`. -/// For `list_branches` it's needed as it compares the current author with commit authors to determine ownership. -fn init_env() { - for (name, value) in [ - ("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000"), - ("GIT_AUTHOR_EMAIL", "author@example.com"), - ("GIT_AUTHOR_NAME", "author"), - ("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000"), - ("GIT_COMMITTER_EMAIL", "committer@example.com"), - ("GIT_COMMITTER_NAME", "committer"), - ] { - std::env::set_var(name, value); - } +#[test] +#[ignore = "TBD"] +fn push_to_two_remotes() -> Result<()> { + Ok(()) } -fn default_author() -> Author { - Author { - name: Some("author".into()), - email: Some("author@example.com".into()), - } +#[test] +#[ignore = "TBD"] +fn own_branch_without_virtual_branch() -> Result<()> { + Ok(()) } -fn project_ctx(name: &str) -> anyhow::Result { - gitbutler_testsupport::read_only::fixture("for-listing.sh", name) +mod util { + use bstr::BString; + use gitbutler_branch_actions::{Author, BranchListing}; + use gitbutler_command_context::CommandContext; + + /// A flattened and simplified mirror of `BranchListing` for comparing the actual and expected data. + #[derive(Default, Debug, PartialEq)] + pub struct ExpectedBranchListing<'a> { + pub identity: &'a str, + pub remotes: Vec<&'a str>, + pub virtual_branch_given_name: Option<&'a str>, + pub virtual_branch_in_workspace: bool, + pub number_of_commits: usize, + pub authors: Vec, + pub own_branch: bool, + } + + pub fn assert_equal( + BranchListing { + name, + remotes, + virtual_branch, + number_of_commits, + updated_at: _, + authors, + own_branch, + head: _, // NOTE: can't have stable commits while `gitbutler-change-id` is not stable/is a UUID. + }: &BranchListing, + mut expected: ExpectedBranchListing, + msg: &str, + ) { + assert_eq!(*name, expected.identity, "{msg}"); + assert_eq!( + *remotes, + expected + .remotes + .into_iter() + .map(BString::from) + .collect::>(), + "{msg}" + ); + assert_eq!( + virtual_branch.as_ref().map(|b| b.given_name.as_str()), + expected.virtual_branch_given_name, + "{msg}" + ); + assert_eq!( + virtual_branch.as_ref().map_or(false, |b| b.in_workspace), + expected.virtual_branch_in_workspace, + "{msg}" + ); + assert_eq!(*number_of_commits, expected.number_of_commits, "{msg}"); + if expected.number_of_commits > 0 && expected.authors.is_empty() { + expected.authors = vec![default_author()]; + } + assert_eq!(*authors, expected.authors, "{msg}"); + if expected.virtual_branch_given_name.is_some() { + expected.own_branch = true; + } + assert_eq!(*own_branch, expected.own_branch, "{msg}"); + } + + /// This function affects all tests, but those who care should just call it, assuming + /// they all care for the same default value. + /// If not, they should be placed in their own integration test or run with `#[serial_test:serial]`. + /// For `list_branches` it's needed as it compares the current author with commit authors to determine ownership. + pub fn init_env() { + for (name, value) in [ + ("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000"), + ("GIT_AUTHOR_EMAIL", "author@example.com"), + ("GIT_AUTHOR_NAME", "author"), + ("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000"), + ("GIT_COMMITTER_EMAIL", "committer@example.com"), + ("GIT_COMMITTER_NAME", "committer"), + ] { + std::env::set_var(name, value); + } + } + + pub fn default_author() -> Author { + Author { + name: Some("author".into()), + email: Some("author@example.com".into()), + } + } + + pub fn project_ctx(name: &str) -> anyhow::Result { + gitbutler_testsupport::read_only::fixture("for-listing.sh", name) + } } +use util::{assert_equal, init_env, project_ctx, ExpectedBranchListing}; diff --git a/crates/gitbutler-commit/src/commit_headers.rs b/crates/gitbutler-commit/src/commit_headers.rs index 8c66fbbae..c9b979506 100644 --- a/crates/gitbutler-commit/src/commit_headers.rs +++ b/crates/gitbutler-commit/src/commit_headers.rs @@ -27,6 +27,9 @@ impl Default for CommitHeadersV2 { fn default() -> Self { CommitHeadersV2 { // Change ID using base16 encoding + // NOTE(ST): Ideally, this could be a computed hash based on the patch applied, similar + // to what would happen during a rebase (if that is even the intention). + // That way, they would be stable, so tests could have reproducible hashes as well. change_id: Uuid::new_v4().to_string(), } } From d0e10ff570bb8ad2df21a73ed03965fc9bbc025f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Aug 2024 15:22:03 +0200 Subject: [PATCH 3/6] make it possible to show remotes for virtual branches that are targets, too. --- crates/gitbutler-branch-actions/src/branch.rs | 34 ++++++++++--------- .../tests/virtual_branches/list.rs | 3 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/crates/gitbutler-branch-actions/src/branch.rs b/crates/gitbutler-branch-actions/src/branch.rs index 0a278a038..e8e7550a5 100644 --- a/crates/gitbutler-branch-actions/src/branch.rs +++ b/crates/gitbutler-branch-actions/src/branch.rs @@ -88,7 +88,7 @@ fn combine_branches( continue; }; // Skip branches that should not be listed, e.g. the target 'main' or the gitbutler technical branches like 'gitbutler/integration' - if !should_list_git_branch(&identity, &target_branch, &branch) { + if !should_list_git_branch(&identity) { continue; } groups.entry(identity).or_default().push(branch); @@ -103,11 +103,12 @@ fn combine_branches( &identity, group_branches, repo, + &remotes, &local_author, - target_branch.sha, + &target_branch, ); match res { - Ok(branch_entry) => Some(branch_entry), + Ok(branch_entry) => branch_entry, Err(err) => { tracing::warn!( "Failed to process branch group {:?} to branch entry: {}", @@ -126,9 +127,10 @@ fn branch_group_to_branch( identity: &str, group_branches: Vec, repo: &git2::Repository, + remotes: &git2::string_array::StringArray, local_author: &git2::Signature, - target_sha: git2::Oid, -) -> Result { + target: &Target, +) -> Result> { let virtual_branch = group_branches.iter().find_map(|branch| match branch { GroupBranch::Virtual(vb) => Some(vb), _ => None, @@ -148,6 +150,14 @@ fn branch_group_to_branch( }) .collect(); + if virtual_branch.is_none() + && local_branches + .iter() + .any(|b| b.get().given_name(remotes).as_deref().ok() == Some(target.branch.branch())) + { + return Ok(None); + } + // Virtual branch associated with this branch let virtual_branch_reference = virtual_branch.map(|branch| VirtualBranchReference { given_name: branch.name.clone(), @@ -184,7 +194,7 @@ fn branch_group_to_branch( virtual_branch.map_or(0, |x| x.updated_timestamp_ms), ); // If no merge base can be found, return with zero stats - let branch = if let Ok(base) = repo.merge_base(target_sha, head) { + let branch = if let Ok(base) = repo.merge_base(target.sha, head) { let mut revwalk = repo.revwalk()?; revwalk.push(head)?; revwalk.hide(base)?; @@ -225,7 +235,7 @@ fn branch_group_to_branch( head, } }; - Ok(branch) + Ok(Some(branch)) } /// A sum type of branch that can be a plain git branch or a virtual branch @@ -255,19 +265,11 @@ impl GroupBranch<'_> { } } } - - /// Returns true if this is a virtual branch. - fn is_virtual(&self) -> bool { - matches!(self, Self::Virtual(_)) - } } /// Determines if a branch should be listed in the UI. /// This excludes the target branch as well as gitbutler specific branches. -fn should_list_git_branch(identity: &str, target: &Target, branch: &GroupBranch) -> bool { - if !branch.is_virtual() && identity == target.branch.branch() { - return false; - } +fn should_list_git_branch(identity: &str) -> bool { // Exclude gitbutler technical branches (not useful for the user) let is_technical = [ "gitbutler/integration", diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs index 78197e4a9..9faadcc1d 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs @@ -100,8 +100,7 @@ fn one_feature_branch_and_one_vbranch_on_integration_one_commit() -> Result<()> &list[0], ExpectedBranchListing { identity: "main", - remotes: vec![], - // remotes: vec!["origin"], // TODO: should be this + remotes: vec!["origin"], virtual_branch_given_name: Some("main"), virtual_branch_in_workspace: true, number_of_commits: 1, From 0cee9378cc4fcd3ea16cc76ede62bf5857646d17 Mon Sep 17 00:00:00 2001 From: Mattias Granlund Date: Thu, 1 Aug 2024 15:10:05 +0100 Subject: [PATCH 4/6] Switch tooltip from mouseover to pointerenter - makes tooltip work on disabled buttons --- packages/ui/src/lib/utils/tooltip.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/lib/utils/tooltip.ts b/packages/ui/src/lib/utils/tooltip.ts index 13e580874..de6763090 100644 --- a/packages/ui/src/lib/utils/tooltip.ts +++ b/packages/ui/src/lib/utils/tooltip.ts @@ -32,14 +32,14 @@ export function tooltip(node: HTMLElement, optsOrString: ToolTipOptions | string setOpts(optsOrString); - function onMouseOver() { + function onPointerEnter() { // If tooltip is displayed we clear hide timeout if (tooltip && timeoutId) clearTimeout(timeoutId); // If no tooltip and no timeout id we set a show timeout else if (!tooltip && !timeoutId) timeoutId = setTimeout(() => show(), delay); } - function onMouseLeave() { + function onPointerLeave() { // If tooltip shown when mouse out then we hide after delay if (tooltip) hide(); // But if we mouse out before tooltip is shown, we cancel the show timer @@ -102,8 +102,8 @@ export function tooltip(node: HTMLElement, optsOrString: ToolTipOptions | string tooltip.style.left = `${leftPos}px`; } - node.addEventListener('mouseover', onMouseOver); - node.addEventListener('mouseleave', onMouseLeave); + node.addEventListener('pointerenter', onPointerEnter); + node.addEventListener('pointerleave', onPointerLeave); return { update(opts: ToolTipOptions | string | undefined) { @@ -112,8 +112,8 @@ export function tooltip(node: HTMLElement, optsOrString: ToolTipOptions | string destroy() { tooltip?.remove(); timeoutId && clearTimeout(timeoutId); - node.removeEventListener('mouseover', onMouseOver); - node.removeEventListener('mouseleave', onMouseLeave); + node.removeEventListener('pointerenter', onPointerEnter); + node.removeEventListener('pointerleave', onPointerLeave); } }; } From 949ac1698dac80436c303d1bb7fbd13aec7fe4d6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Aug 2024 15:35:05 +0200 Subject: [PATCH 5/6] add more tests for a deeper understanding on the graph traversal. --- crates/gitbutler-branch-actions/src/branch.rs | 10 ++- .../tests/fixtures/for-listing.sh | 21 +++++ .../tests/virtual_branches/list.rs | 82 +++++++++++++++---- 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/crates/gitbutler-branch-actions/src/branch.rs b/crates/gitbutler-branch-actions/src/branch.rs index e8e7550a5..aaf81b952 100644 --- a/crates/gitbutler-branch-actions/src/branch.rs +++ b/crates/gitbutler-branch-actions/src/branch.rs @@ -4,7 +4,7 @@ use std::{ vec, }; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use bstr::{BString, ByteSlice}; use gitbutler_branch::{Branch as GitButlerBranch, BranchId, ReferenceExt, Target}; use gitbutler_command_context::CommandContext; @@ -131,10 +131,14 @@ fn branch_group_to_branch( local_author: &git2::Signature, target: &Target, ) -> Result> { - let virtual_branch = group_branches.iter().find_map(|branch| match branch { + let mut vbranches = group_branches.iter().filter_map(|branch| match branch { GroupBranch::Virtual(vb) => Some(vb), _ => None, }); + let virtual_branch = vbranches.next(); + if vbranches.next().is_some() { + bail!("Found more than one virtual branch with the same identity - this shouldn't be possible") + } let remote_branches: Vec<&git2::Branch> = group_branches .iter() .filter_map(|branch| match branch { @@ -206,7 +210,7 @@ fn branch_group_to_branch( commits.push(commit); } // If there are no commits (i.e. virtual branch only) it is considered the users own - let own_branch = commits.is_empty() + let own_branch = (virtual_branch.is_some() && commits.is_empty()) || commits.iter().any(|commit| { let commit_author = commit.author(); local_author.name_bytes() == commit_author.name_bytes() diff --git a/crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh b/crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh index 56058b366..83808a8e9 100644 --- a/crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh +++ b/crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh @@ -61,3 +61,24 @@ git clone remote a-vbranch-named-like-target-branch-short-name tick $CLI branch commit main -m "virtual branch change in index and worktree" ) + +git clone remote one-vbranch-on-integration-two-remotes +(cd one-vbranch-on-integration-two-remotes + $CLI project add --switch-to-integration "$(git rev-parse --symbolic-full-name @{u})" + $CLI branch create main + + git remote add other-remote ../remote + git fetch other-remote +) + +git clone remote one-branch-one-commit-other-branch-without-commit +(cd one-branch-one-commit-other-branch-without-commit + local_tracking_ref="$(git rev-parse --symbolic-full-name @{u})"; + + git checkout -b feature main + echo change >> file + git add . && git commit -m "change standard git feature branch" + + git checkout -b other-feature main + $CLI project add --switch-to-integration "$local_tracking_ref" +) diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs index 9faadcc1d..041fc7a29 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/list.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use gitbutler_branch_actions::{list_branches, BranchListingFilter}; +use gitbutler_branch_actions::BranchListingFilter; #[test] fn one_vbranch_on_integration() -> Result<()> { @@ -94,7 +94,7 @@ fn one_feature_branch_and_one_vbranch_on_integration_one_commit() -> Result<()> assert_eq!( list.len(), 1, - "it finds our single virtual branch despit it having the same 'identity' as the target branch: 'main'" + "it finds our single virtual branch despite it having the same 'identity' as the target branch: 'main'" ); assert_equal( &list[0], @@ -114,20 +114,60 @@ fn one_feature_branch_and_one_vbranch_on_integration_one_commit() -> Result<()> } #[test] -#[ignore = "TBD"] -fn push_to_two_remotes() -> Result<()> { +fn one_branch_on_integration_multiple_remotes() -> Result<()> { + init_env(); + let ctx = project_ctx("one-vbranch-on-integration-two-remotes")?; + let list = list_branches(&ctx, None)?; + assert_eq!(list.len(), 1, "a single virtual branch"); + + assert_equal( + &list[0], + ExpectedBranchListing { + identity: "main", + remotes: vec!["other-remote", "origin"], + virtual_branch_given_name: Some("main"), + virtual_branch_in_workspace: true, + ..Default::default() + }, + "multiple remotes are detected", + ); Ok(()) } #[test] -#[ignore = "TBD"] -fn own_branch_without_virtual_branch() -> Result<()> { +fn own_branch_one_commit_other_branch_without_commit_without_virtual_branch() -> Result<()> { + init_env(); + let ctx = project_ctx("one-branch-one-commit-other-branch-without-commit")?; + let list = list_branches(&ctx, None)?; + assert_eq!(list.len(), 2, "two local branches"); + + assert_equal( + &list[0], + ExpectedBranchListing { + identity: "feature", + number_of_commits: 1, + own_branch: true, + ..Default::default() + }, + "a local ref can be owned if there are commits", + ); + assert_equal( + &list[1], + ExpectedBranchListing { + identity: "other-feature", + number_of_commits: 0, + own_branch: false, + ..Default::default() + }, + "a local ref is not owned without commits", + ); Ok(()) } mod util { + use anyhow::Result; use bstr::BString; - use gitbutler_branch_actions::{Author, BranchListing}; + use gitbutler_branch_actions::{Author, BranchListing, BranchListingFilter}; use gitbutler_command_context::CommandContext; /// A flattened and simplified mirror of `BranchListing` for comparing the actual and expected data. @@ -156,7 +196,7 @@ mod util { mut expected: ExpectedBranchListing, msg: &str, ) { - assert_eq!(*name, expected.identity, "{msg}"); + assert_eq!(*name, expected.identity, "identity: {msg}"); assert_eq!( *remotes, expected @@ -164,23 +204,26 @@ mod util { .into_iter() .map(BString::from) .collect::>(), - "{msg}" + "remotes: {msg}" ); assert_eq!( virtual_branch.as_ref().map(|b| b.given_name.as_str()), expected.virtual_branch_given_name, - "{msg}" + "virtual-branch-name: {msg}" ); assert_eq!( virtual_branch.as_ref().map_or(false, |b| b.in_workspace), expected.virtual_branch_in_workspace, - "{msg}" + "virtual-branch-in-workspace: {msg}" + ); + assert_eq!( + *number_of_commits, expected.number_of_commits, + "number-of-commits: {msg}" ); - assert_eq!(*number_of_commits, expected.number_of_commits, "{msg}"); if expected.number_of_commits > 0 && expected.authors.is_empty() { expected.authors = vec![default_author()]; } - assert_eq!(*authors, expected.authors, "{msg}"); + assert_eq!(*authors, expected.authors, "authors: {msg}"); if expected.virtual_branch_given_name.is_some() { expected.own_branch = true; } @@ -211,8 +254,17 @@ mod util { } } - pub fn project_ctx(name: &str) -> anyhow::Result { + pub fn project_ctx(name: &str) -> Result { gitbutler_testsupport::read_only::fixture("for-listing.sh", name) } + + pub fn list_branches( + ctx: &CommandContext, + filter: Option, + ) -> Result> { + let mut branches = gitbutler_branch_actions::list_branches(ctx, filter)?; + branches.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(branches) + } } -use util::{assert_equal, init_env, project_ctx, ExpectedBranchListing}; +use util::{assert_equal, init_env, list_branches, project_ctx, ExpectedBranchListing}; From cda04e9b5b869edb23fd8c73d7fa03287bed0ad3 Mon Sep 17 00:00:00 2001 From: Nico Domino Date: Thu, 1 Aug 2024 16:54:49 +0200 Subject: [PATCH 6/6] feat: clone repo onboarding step (#4542) Co-authored-by: Pavel Laptev --- .../lib/assets/illustrations/analytics.svg | 66 +++--- .../lib/assets/illustrations/cloning-repo.svg | 49 ++++ .../lib/assets/no-projects/new-project.svg | 9 - .../src/lib/assets/no-projects/signin.svg | 6 - apps/desktop/src/lib/assets/signin.svg | 6 + .../src/lib/assets/welcome/clone-repo.svg | 7 + .../lib/assets/welcome/new-local-project.svg | 7 + apps/desktop/src/lib/backend/projects.ts | 8 +- .../lib/barmenuActions/FileMenuAction.svelte | 39 +++ .../ProjectSettingsMenuAction.svelte | 69 ++++++ .../ProjectSettingsMenuAction.svelte | 30 --- .../lib/components/ProjectSetupTarget.svelte | 6 +- .../src/lib/components/ProjectSwitcher.svelte | 26 +- .../components/SegmentControl/Segment.svelte | 138 ----------- .../SegmentControl/SegmentedControl.svelte | 58 ----- .../lib/components/SegmentControl/segment.ts | 15 -- .../desktop/src/lib/components/Welcome.svelte | 59 ++++- .../src/lib/components/WelcomeAction.svelte | 80 +++++-- .../lib/components/WelcomeSigninAction.svelte | 23 +- apps/desktop/src/lib/icons/icons.json | 4 +- .../src/lib/navigation/ProjectsPopup.svelte | 28 ++- .../src/lib/onboarding/CloneForm.svelte | 223 ++++++++++++++++++ apps/desktop/src/lib/shared/IconLink.svelte | 9 +- apps/desktop/src/lib/shared/Spacer.svelte | 29 ++- apps/desktop/src/lib/shared/TextBox.svelte | 10 + apps/desktop/src/lib/url/gitUrl.ts | 4 +- apps/desktop/src/lib/utils/toasts.ts | 2 +- apps/desktop/src/routes/+page.svelte | 23 +- .../src/routes/[projectId]/+layout.svelte | 49 ++-- .../src/routes/onboarding/+page.svelte | 18 ++ .../src/routes/onboarding/clone/+page.svelte | 9 + crates/gitbutler-tauri/src/main.rs | 1 + crates/gitbutler-tauri/src/menu.rs | 23 ++ crates/gitbutler-tauri/src/repo.rs | 9 + .../ui/src/lib/SegmentControl/Segment.svelte | 22 +- .../lib/SegmentControl/SegmentControl.svelte | 6 +- .../SegmentControl/SegmentControl.stories.ts | 2 +- .../SegmentControl/SegmentControl.svelte | 6 +- .../ui/src/styles/components/commit-lines.css | 1 - packages/ui/src/styles/utility/text.css | 1 + 40 files changed, 761 insertions(+), 419 deletions(-) create mode 100644 apps/desktop/src/lib/assets/illustrations/cloning-repo.svg delete mode 100644 apps/desktop/src/lib/assets/no-projects/new-project.svg delete mode 100644 apps/desktop/src/lib/assets/no-projects/signin.svg create mode 100644 apps/desktop/src/lib/assets/signin.svg create mode 100644 apps/desktop/src/lib/assets/welcome/clone-repo.svg create mode 100644 apps/desktop/src/lib/assets/welcome/new-local-project.svg create mode 100644 apps/desktop/src/lib/barmenuActions/FileMenuAction.svelte create mode 100644 apps/desktop/src/lib/barmenuActions/ProjectSettingsMenuAction.svelte delete mode 100644 apps/desktop/src/lib/components/ProjectSettingsMenuAction.svelte delete mode 100644 apps/desktop/src/lib/components/SegmentControl/Segment.svelte delete mode 100644 apps/desktop/src/lib/components/SegmentControl/SegmentedControl.svelte delete mode 100644 apps/desktop/src/lib/components/SegmentControl/segment.ts create mode 100644 apps/desktop/src/lib/onboarding/CloneForm.svelte create mode 100644 apps/desktop/src/routes/onboarding/+page.svelte create mode 100644 apps/desktop/src/routes/onboarding/clone/+page.svelte diff --git a/apps/desktop/src/lib/assets/illustrations/analytics.svg b/apps/desktop/src/lib/assets/illustrations/analytics.svg index b4c12dc29..0c5519099 100644 --- a/apps/desktop/src/lib/assets/illustrations/analytics.svg +++ b/apps/desktop/src/lib/assets/illustrations/analytics.svg @@ -1,35 +1,35 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src/lib/assets/illustrations/cloning-repo.svg b/apps/desktop/src/lib/assets/illustrations/cloning-repo.svg new file mode 100644 index 000000000..799f089dd --- /dev/null +++ b/apps/desktop/src/lib/assets/illustrations/cloning-repo.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src/lib/assets/no-projects/new-project.svg b/apps/desktop/src/lib/assets/no-projects/new-project.svg deleted file mode 100644 index e905b6c49..000000000 --- a/apps/desktop/src/lib/assets/no-projects/new-project.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/apps/desktop/src/lib/assets/no-projects/signin.svg b/apps/desktop/src/lib/assets/no-projects/signin.svg deleted file mode 100644 index c7726f33d..000000000 --- a/apps/desktop/src/lib/assets/no-projects/signin.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/desktop/src/lib/assets/signin.svg b/apps/desktop/src/lib/assets/signin.svg new file mode 100644 index 000000000..6f11cf8aa --- /dev/null +++ b/apps/desktop/src/lib/assets/signin.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/desktop/src/lib/assets/welcome/clone-repo.svg b/apps/desktop/src/lib/assets/welcome/clone-repo.svg new file mode 100644 index 000000000..b212bdd22 --- /dev/null +++ b/apps/desktop/src/lib/assets/welcome/clone-repo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/desktop/src/lib/assets/welcome/new-local-project.svg b/apps/desktop/src/lib/assets/welcome/new-local-project.svg new file mode 100644 index 000000000..9054f06ad --- /dev/null +++ b/apps/desktop/src/lib/assets/welcome/new-local-project.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/desktop/src/lib/backend/projects.ts b/apps/desktop/src/lib/backend/projects.ts index 546a77824..f33a4ca89 100644 --- a/apps/desktop/src/lib/backend/projects.ts +++ b/apps/desktop/src/lib/backend/projects.ts @@ -106,9 +106,11 @@ export class ProjectService { await invoke('open_project_in_window', { id: projectId }); } - async addProject() { - const path = await this.promptForDirectory(); - if (!path) return; + async addProject(path?: string) { + if (!path) { + path = await this.promptForDirectory(); + if (!path) return; + } if (!this.validateProjectPath(path)) return; diff --git a/apps/desktop/src/lib/barmenuActions/FileMenuAction.svelte b/apps/desktop/src/lib/barmenuActions/FileMenuAction.svelte new file mode 100644 index 000000000..77dfc94f7 --- /dev/null +++ b/apps/desktop/src/lib/barmenuActions/FileMenuAction.svelte @@ -0,0 +1,39 @@ + + + diff --git a/apps/desktop/src/lib/barmenuActions/ProjectSettingsMenuAction.svelte b/apps/desktop/src/lib/barmenuActions/ProjectSettingsMenuAction.svelte new file mode 100644 index 000000000..4acd97e27 --- /dev/null +++ b/apps/desktop/src/lib/barmenuActions/ProjectSettingsMenuAction.svelte @@ -0,0 +1,69 @@ + + + diff --git a/apps/desktop/src/lib/components/ProjectSettingsMenuAction.svelte b/apps/desktop/src/lib/components/ProjectSettingsMenuAction.svelte deleted file mode 100644 index cf288f4a5..000000000 --- a/apps/desktop/src/lib/components/ProjectSettingsMenuAction.svelte +++ /dev/null @@ -1,30 +0,0 @@ - diff --git a/apps/desktop/src/lib/components/ProjectSetupTarget.svelte b/apps/desktop/src/lib/components/ProjectSetupTarget.svelte index 406a7afd6..3e8eb3ea5 100644 --- a/apps/desktop/src/lib/components/ProjectSetupTarget.svelte +++ b/apps/desktop/src/lib/components/ProjectSetupTarget.svelte @@ -247,8 +247,8 @@ -
- Back +
+ Cancel - - diff --git a/apps/desktop/src/lib/components/SegmentControl/SegmentedControl.svelte b/apps/desktop/src/lib/components/SegmentControl/SegmentedControl.svelte deleted file mode 100644 index f75b40fde..000000000 --- a/apps/desktop/src/lib/components/SegmentControl/SegmentedControl.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -
- -
- - diff --git a/apps/desktop/src/lib/components/SegmentControl/segment.ts b/apps/desktop/src/lib/components/SegmentControl/segment.ts deleted file mode 100644 index 00d8003a7..000000000 --- a/apps/desktop/src/lib/components/SegmentControl/segment.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Writable } from 'svelte/store'; - -export interface SegmentItem { - id: string; - index: number; - disabled: boolean; -} -export interface SegmentContext { - focusedSegmentIndex: Writable; - selectedSegmentIndex: Writable; - length: Writable; - setIndex(): number; - addSegment(segment: SegmentItem): void; - setSelected(index: number): void; -} diff --git a/apps/desktop/src/lib/components/Welcome.svelte b/apps/desktop/src/lib/components/Welcome.svelte index ec40f38f6..a6fd65f2d 100644 --- a/apps/desktop/src/lib/components/Welcome.svelte +++ b/apps/desktop/src/lib/components/Welcome.svelte @@ -1,14 +1,17 @@

Welcome to GitButler

- - - {@html newProjectSvg} - - - Verify valid Git repository in selected folder before importing. - - +
+ + {#snippet icon()} + {@html newProjectSvg} + {/snippet} + {#snippet message()} + Should be a valid git repository + {/snippet} + + + {#snippet icon()} + {@html cloneRepoSvg} + {/snippet} + {#snippet message()} + Clone a repo using a URL + {/snippet} + +
@@ -56,6 +83,7 @@ Discord X Instagram + YouTube
@@ -78,11 +106,16 @@ margin-top: 32px; } + .welcome__actions--repo { + display: flex; + gap: 8px; + } + .links { display: flex; gap: 56px; padding: 28px; - background: var(--clr-bg-2); + background: var(--clr-bg-1-muted); border-radius: var(--radius-m); margin-top: 20px; } @@ -102,11 +135,13 @@ } .community-links { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(2, 1fr); column-gap: 12px; row-gap: 4px; max-width: 192px; margin-left: -6px; } + + /* SMALL ILLUSTRATIONS */ diff --git a/apps/desktop/src/lib/components/WelcomeAction.svelte b/apps/desktop/src/lib/components/WelcomeAction.svelte index b75aec44d..18c9b9694 100644 --- a/apps/desktop/src/lib/components/WelcomeAction.svelte +++ b/apps/desktop/src/lib/components/WelcomeAction.svelte @@ -1,18 +1,46 @@ - diff --git a/apps/desktop/src/lib/components/WelcomeSigninAction.svelte b/apps/desktop/src/lib/components/WelcomeSigninAction.svelte index 99ce8ada4..c1aab9dbf 100644 --- a/apps/desktop/src/lib/components/WelcomeSigninAction.svelte +++ b/apps/desktop/src/lib/components/WelcomeSigninAction.svelte @@ -1,11 +1,16 @@ + +

Clone a repository

+
+
+
+ { + selectedRemoteType = id as keyof typeof RemoteType; + }} + > + HTTP + SSH + +
+
+
+ +
+
+
Where to clone
+ + +
+
+ + + +{#if completed} + {@render Notification({ title: 'Success', style: 'success' })} +{/if} +{#if errors.length} + {@render Notification({ title: 'Error', items: errors, style: 'error' })} +{/if} + +
+ + +
+ +{#snippet Notification({ title, items, style }: { title: string, items?: any[], style: MessageStyle})} +
+ + + {title} + + + {#if items && items.length > 0} + {#each items as item} + {@html item.label} + {/each} + {/if} + + +
+{/snippet} + + diff --git a/apps/desktop/src/lib/shared/IconLink.svelte b/apps/desktop/src/lib/shared/IconLink.svelte index 688ca8223..1821867e5 100644 --- a/apps/desktop/src/lib/shared/IconLink.svelte +++ b/apps/desktop/src/lib/shared/IconLink.svelte @@ -3,12 +3,11 @@ import Icon from '$lib/shared/Icon.svelte'; import type iconsJson from '$lib/icons/icons.json'; - export let iconOpacity: number = 0.7; export let icon: keyof typeof iconsJson; - + @@ -16,12 +15,18 @@ .link { display: flex; align-items: center; + width: fit-content; gap: 10px; padding: 4px 6px; border-radius: var(--radius-m); + + color: var(--clr-scale-ntrl-40); + text-decoration: none; + transition: background-color var(--transition-fast); &:hover { + color: var(--clr-text-1); background-color: oklch(from var(--clr-scale-ntrl-0) l c h / 0.05); } } diff --git a/apps/desktop/src/lib/shared/Spacer.svelte b/apps/desktop/src/lib/shared/Spacer.svelte index f8d7549f6..f5d42e57b 100644 --- a/apps/desktop/src/lib/shared/Spacer.svelte +++ b/apps/desktop/src/lib/shared/Spacer.svelte @@ -1,7 +1,13 @@ -
+
diff --git a/apps/desktop/src/lib/shared/TextBox.svelte b/apps/desktop/src/lib/shared/TextBox.svelte index ead16e45d..c433d0b70 100644 --- a/apps/desktop/src/lib/shared/TextBox.svelte +++ b/apps/desktop/src/lib/shared/TextBox.svelte @@ -11,6 +11,7 @@ export let width: number | undefined = undefined; export let textAlign: 'left' | 'center' | 'right' = 'left'; export let placeholder: string | undefined = undefined; + export let helperText: string | undefined = undefined; export let label: string | undefined = undefined; export let reversedDirection: boolean = false; export let wide: boolean = false; @@ -139,6 +140,10 @@ {/if} + + {#if helperText} +

{helperText}

+ {/if} @@ -178,6 +183,11 @@ color: var(--clr-scale-ntrl-50); } + .textbox__helper-text { + color: var(--clr-scale-ntrl-50); + margin-top: 6px; + } + .textbox__icon { z-index: var(--z-ground); pointer-events: none; diff --git a/apps/desktop/src/lib/url/gitUrl.ts b/apps/desktop/src/lib/url/gitUrl.ts index 34dba3937..c707ec604 100644 --- a/apps/desktop/src/lib/url/gitUrl.ts +++ b/apps/desktop/src/lib/url/gitUrl.ts @@ -5,11 +5,13 @@ export type RepoInfo = { name: string; owner: string; organization?: string; + protocol?: string; }; export function parseRemoteUrl(url: string): RepoInfo { - const { source, name, owner, organization } = gitUrlParse(url); + const { protocol, source, name, owner, organization } = gitUrlParse(url); return { + protocol, source, name, owner, diff --git a/apps/desktop/src/lib/utils/toasts.ts b/apps/desktop/src/lib/utils/toasts.ts index f3294d91d..73c480650 100644 --- a/apps/desktop/src/lib/utils/toasts.ts +++ b/apps/desktop/src/lib/utils/toasts.ts @@ -2,7 +2,7 @@ import toast, { type ToastOptions, type ToastPosition } from 'svelte-french-toas const defaultOptions = { position: 'bottom-center' as ToastPosition, - style: 'border-radius: 8px; background: black; color: #fff;' + style: 'border-radius: 8px; background: black; color: #fff; font-size: 0.813em;' }; export function error(msg: string, options: ToastOptions = {}) { diff --git a/apps/desktop/src/routes/+page.svelte b/apps/desktop/src/routes/+page.svelte index aad2999eb..94bc174d5 100644 --- a/apps/desktop/src/routes/+page.svelte +++ b/apps/desktop/src/routes/+page.svelte @@ -1,12 +1,6 @@ {#if $redirect === undefined} -{:else if !$analyticsConfirmed} - - - -{:else if $redirect === null} - - - {/if} diff --git a/apps/desktop/src/routes/[projectId]/+layout.svelte b/apps/desktop/src/routes/[projectId]/+layout.svelte index f1ee71d6c..de01a6387 100644 --- a/apps/desktop/src/routes/[projectId]/+layout.svelte +++ b/apps/desktop/src/routes/[projectId]/+layout.svelte @@ -1,6 +1,7 @@ - - {#key projectId} - + ($showHistoryView = show)} + /> + {#if !project}

Project not found!

{:else if $baseError instanceof NoDefaultTarget} - {:else if $baseError} diff --git a/apps/desktop/src/routes/onboarding/+page.svelte b/apps/desktop/src/routes/onboarding/+page.svelte new file mode 100644 index 000000000..399176d7e --- /dev/null +++ b/apps/desktop/src/routes/onboarding/+page.svelte @@ -0,0 +1,18 @@ + + + + {#if $analyticsConfirmed} + + {:else} + + {/if} + diff --git a/apps/desktop/src/routes/onboarding/clone/+page.svelte b/apps/desktop/src/routes/onboarding/clone/+page.svelte new file mode 100644 index 000000000..18e0180f1 --- /dev/null +++ b/apps/desktop/src/routes/onboarding/clone/+page.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/crates/gitbutler-tauri/src/main.rs b/crates/gitbutler-tauri/src/main.rs index d3692beaa..70198202a 100644 --- a/crates/gitbutler-tauri/src/main.rs +++ b/crates/gitbutler-tauri/src/main.rs @@ -146,6 +146,7 @@ fn main() { repo::commands::git_get_local_config, repo::commands::git_set_local_config, repo::commands::check_signing_settings, + repo::commands::git_clone_repository, virtual_branches::commands::list_virtual_branches, virtual_branches::commands::create_virtual_branch, virtual_branches::commands::commit_virtual_branch, diff --git a/crates/gitbutler-tauri/src/menu.rs b/crates/gitbutler-tauri/src/menu.rs index dd3402d7b..574f005f6 100644 --- a/crates/gitbutler-tauri/src/menu.rs +++ b/crates/gitbutler-tauri/src/menu.rs @@ -83,14 +83,27 @@ pub fn build(_package_info: &PackageInfo) -> Menu { } let mut file_menu = Menu::new(); + + file_menu = file_menu.add_item( + CustomMenuItem::new("file/add-local-repo", "Add Local Repository…") + .accelerator("CmdOrCtrl+O"), + ); + + file_menu = file_menu.add_item( + CustomMenuItem::new("file/clone-repo", "Clone Repository…") + .accelerator("CmdOrCtrl+Shift+O"), + ); + #[cfg(target_os = "macos")] { // NB: macOS has the concept of having an app running but its // window closed, but other platforms do not + file_menu = file_menu.add_native_item(MenuItem::Separator); file_menu = file_menu.add_native_item(MenuItem::CloseWindow); } #[cfg(not(target_os = "macos"))] { + file_menu = file_menu.add_native_item(MenuItem::Separator); file_menu = file_menu.add_native_item(MenuItem::Quit); } @@ -199,6 +212,16 @@ fn disabled_menu_item(id: &str, title: &str) -> CustomMenuItem { } pub fn handle_event(event: &WindowMenuEvent) { + if event.menu_item_id() == "file/add-local-repo" { + emit(event.window(), "menu://file/add-local-repo/clicked"); + return; + } + + if event.menu_item_id() == "file/clone-repo" { + emit(event.window(), "menu://file/clone-repo/clicked"); + return; + } + #[cfg(any(debug_assertions, feature = "devtools"))] { if event.menu_item_id() == "view/devtools" { diff --git a/crates/gitbutler-tauri/src/repo.rs b/crates/gitbutler-tauri/src/repo.rs index 872f83a3f..d23b52a39 100644 --- a/crates/gitbutler-tauri/src/repo.rs +++ b/crates/gitbutler-tauri/src/repo.rs @@ -1,7 +1,10 @@ pub mod commands { + use anyhow::{Context, Result}; + use git2::{self}; use gitbutler_project as projects; use gitbutler_project::ProjectId; use gitbutler_repo::RepoCommands; + use std::path::Path; use tauri::State; use tracing::instrument; @@ -39,4 +42,10 @@ pub mod commands { let project = projects.get(id)?; project.check_signing_settings().map_err(Into::into) } + + #[tauri::command(async)] + pub fn git_clone_repository(repository_url: &str, target_dir: &Path) -> Result<(), Error> { + git2::Repository::clone(repository_url, target_dir).context("Cloning failed")?; + Ok(()) + } } diff --git a/packages/ui/src/lib/SegmentControl/Segment.svelte b/packages/ui/src/lib/SegmentControl/Segment.svelte index 9e63e5dec..5915bde0f 100644 --- a/packages/ui/src/lib/SegmentControl/Segment.svelte +++ b/packages/ui/src/lib/SegmentControl/Segment.svelte @@ -1,4 +1,5 @@ { console.log('Selected index:', id); diff --git a/packages/ui/src/styles/components/commit-lines.css b/packages/ui/src/styles/components/commit-lines.css index 5f99db688..c77073511 100644 --- a/packages/ui/src/styles/components/commit-lines.css +++ b/packages/ui/src/styles/components/commit-lines.css @@ -5,7 +5,6 @@ --border-color: var(--clr-commit-shadow); --border-style: solid; - --border-dashed: dashed; &.none { --border-color: transparent; diff --git a/packages/ui/src/styles/utility/text.css b/packages/ui/src/styles/utility/text.css index 4a290e40c..e3230b50c 100644 --- a/packages/ui/src/styles/utility/text.css +++ b/packages/ui/src/styles/utility/text.css @@ -164,6 +164,7 @@ font-family: var(--serif-font-family); font-size: 2.5rem; line-height: var(--text-body-line-height); + font-weight: 400; } /* modifiers */