fix merge conflict

This commit is contained in:
extrawurst 2023-10-18 16:45:40 +02:00 committed by GitButler
commit bb56719d1e
21 changed files with 1230 additions and 641 deletions

45
Cargo.lock generated
View File

@ -86,9 +86,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.5.0"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
dependencies = [
"anstyle",
"anstyle-parse",
@ -124,9 +124,9 @@ dependencies = [
[[package]]
name = "anstyle-wincon"
version = "2.1.0"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
@ -704,9 +704,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.2"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
dependencies = [
"clap_builder",
"clap_derive",
@ -714,9 +714,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.2"
version = "4.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
dependencies = [
"anstream",
"anstyle",
@ -1164,6 +1164,21 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "deunicode"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71dbf1bf89c23e9cd1baf5e654f622872655f195b36588dc9dc38f7eda30758c"
dependencies = [
"deunicode 1.4.1",
]
[[package]]
name = "deunicode"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a1abaf4d861455be59f64fd2b55606cb151fce304ede7165f410243ce96bde6"
[[package]]
name = "dialoguer"
version = "0.11.0"
@ -1950,6 +1965,7 @@ dependencies = [
"sha1",
"sha2",
"similar",
"slug",
"ssh-key",
"tantivy",
"tauri",
@ -4686,9 +4702,9 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.10.7"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
@ -4785,6 +4801,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slug"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
dependencies = [
"deunicode 0.4.5",
]
[[package]]
name = "smallvec"
version = "1.11.0"

View File

@ -10,7 +10,7 @@ rust-version = "1.57"
[dependencies]
anyhow = "1.0.71"
clap = { version = "4.0", features = ["derive"] }
clap = { version = "4.4", features = ["derive"] }
colored = "2.0.0"
dialoguer = "0.11.0"
dirs = "5.0.1"

View File

@ -81,6 +81,7 @@ impl super::RunCommand for Move {
virtual_branches::update_branch(
&gb_repository,
&app.project_repository(),
virtual_branches::branch::BranchUpdateRequest {
id: target_branch.id,
ownership: Some(ownership),

View File

@ -20,6 +20,7 @@ impl super::RunCommand for New {
virtual_branches::create_virtual_branch(
&app.gb_repository(),
&app.project_repository(),
&virtual_branches::branch::BranchCreateRequest {
name: Some(input),
..Default::default()

View File

@ -49,8 +49,9 @@ serde = { version = "1.0", features = ["derive"] }
serde-jsonlines = "0.4.0"
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
sha1 = "0.10.6"
sha2 = "0.10.7"
sha2 = "0.10.8"
similar = { version = "2.2.1", features = ["unicode"] }
slug = "0.1.4"
ssh-key = { version = "0.6.1", features = [ "alloc", "ed25519" ] }
tantivy = "0.20.2"
tauri = { version = "1.5.2", features = ["dialog-open", "fs-read-file", "path-all", "process-relaunch", "protocol-asset", "shell-open", "system-tray", "window-maximize", "window-start-dragging", "window-unmaximize"] }

View File

@ -386,7 +386,6 @@ impl Repository {
self.0.remote(name, url).map(Into::into).map_err(Into::into)
}
#[cfg(test)]
pub fn references(&self) -> Result<impl Iterator<Item = Result<Reference>>> {
self.0
.references()

View File

@ -213,9 +213,7 @@ impl Repository {
if let Some(url) = remote_url {
match url.scheme {
#[cfg(test)]
git::Scheme::File => Ok(remote),
git::Scheme::Https => Ok(remote),
git::Scheme::Https | git::Scheme::File => Ok(remote),
_ => {
let https_url = url
.as_https()
@ -260,9 +258,7 @@ impl Repository {
if let Some(url) = remote_url {
match url.scheme {
#[cfg(test)]
git::Scheme::File => Ok(remote),
git::Scheme::Ssh => Ok(remote),
git::Scheme::Ssh | git::Scheme::File => Ok(remote),
_ => {
let ssh_url = url
.as_ssh()

View File

@ -18,7 +18,7 @@ pub struct BaseBranch {
pub branch_name: String,
pub remote_name: String,
pub remote_url: String,
pub base_sha: String,
pub base_sha: git::Oid,
pub current_sha: String,
pub behind: usize,
pub upstream_commits: Vec<RemoteCommit>,
@ -483,7 +483,7 @@ pub fn target_to_base_branch(
branch_name: format!("{}/{}", target.branch.remote(), target.branch.branch()),
remote_name: target.branch.remote().to_string(),
remote_url: target.remote_url.clone(),
base_sha: target.sha.to_string(),
base_sha: target.sha,
current_sha: oid.to_string(),
behind: upstream_commits.len(),
upstream_commits,

View File

@ -8,6 +8,7 @@ pub use file_ownership::FileOwnership;
pub use hunk::Hunk;
pub use ownership::Ownership;
pub use reader::BranchReader as Reader;
use slug::slugify;
pub use writer::BranchWriter as Writer;
use std::path;
@ -43,6 +44,12 @@ pub struct Branch {
pub order: usize,
}
impl Branch {
pub fn refname(&self) -> String {
format!("refs/gitbutler/{}", slugify(&self.name))
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct BranchUpdateRequest {
pub id: BranchId,

View File

@ -186,9 +186,10 @@ impl Controller {
if conflicts::is_resolving(project_repository) {
return Err(Error::Conflicting);
}
let branch_id = super::create_virtual_branch(gb_repository, create)
.map_err(Error::Other)?
.id;
let branch_id =
super::create_virtual_branch(gb_repository, project_repository, create)
.map_err(Error::Other)?
.id;
Ok(branch_id)
})
})
@ -356,8 +357,8 @@ impl Controller {
branch_update: super::branch::BranchUpdateRequest,
) -> Result<(), Error> {
self.with_lock(project_id, || {
self.with_verify_branch(project_id, |gb_repository, _, _| {
super::update_branch(gb_repository, branch_update)?;
self.with_verify_branch(project_id, |gb_repository, project_repository, _| {
super::update_branch(gb_repository, project_repository, branch_update)?;
Ok(())
})
})

View File

@ -113,8 +113,7 @@ pub fn update_gitbutler_integration(
for branch in &applied_virtual_branches {
message.push_str(" - ");
message.push_str(branch.name.as_str());
let branch_name = super::name_to_branch(branch.name.as_str());
message.push_str(format!(" (gitbutler/{})", &branch_name).as_str());
message.push_str(format!(" ({})", &branch.refname()).as_str());
message.push('\n');
if branch.head != target.sha {
@ -183,9 +182,12 @@ pub fn update_gitbutler_integration(
branch_head = repo.find_commit(branch_head_oid)?;
}
let branch_name = super::name_to_branch(branch.name.as_str());
let branch_ref = format!("refs/gitbutler/{}", branch_name);
repo.reference(&branch_ref, branch_head.id(), true, "update virtual branch")?;
repo.reference(
&branch.refname(),
branch_head.id(),
true,
"update virtual branch",
)?;
}
Ok(())
@ -282,6 +284,7 @@ fn verify_head_is_clean(
let new_branch = super::create_virtual_branch(
gb_repository,
project_repository,
&BranchCreateRequest {
name: extra_commits
.last()

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ use std::{
use anyhow::{anyhow, bail, Context, Result};
use diffy::{apply_bytes, Patch};
use serde::Serialize;
use slug::slugify;
use crate::{
dedup::{dedup, dedup_fmt},
@ -945,6 +946,7 @@ pub fn commit_to_vbranch_commit(
pub fn create_virtual_branch(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
create: &BranchCreateRequest,
) -> Result<branch::Branch> {
let current_session = gb_repository
@ -996,17 +998,15 @@ pub fn create_virtual_branch(
.context("failed to get elapsed time")?
.as_millis();
let name: String = create.name.as_ref().map_or_else(
|| {
dedup(
&all_virtual_branches
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>(),
"Virtual branch",
)
},
ToString::to_string,
let name = dedup(
&all_virtual_branches
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>(),
create
.name
.as_ref()
.unwrap_or(&"Virtual branch".to_string()),
);
let mut branch = Branch {
@ -1034,6 +1034,11 @@ pub fn create_virtual_branch(
.write(&branch)
.context("failed to write branch")?;
project_repository
.git_repository
.reference(&branch.refname(), branch.head, false, "new vbranch")
.context("failed to create branch reference")?;
Ok(branch)
}
@ -1189,6 +1194,7 @@ pub fn merge_virtual_branch_upstream(
pub fn update_branch(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
branch_update: branch::BranchUpdateRequest,
) -> Result<branch::Branch> {
let current_session = gb_repository
@ -1209,7 +1215,32 @@ pub fn update_branch(
}
if let Some(name) = branch_update.name {
branch.name = name;
let all_virtual_branches = Iterator::new(&current_session_reader)
.context("failed to create branch iterator")?
.collect::<Result<Vec<branch::Branch>, reader::Error>>()
.context("failed to read virtual branches")?;
if let Ok(ref mut reference) = project_repository
.git_repository
.find_reference(&branch.refname())
{
reference
.delete()
.context("failed to delete old branch reference")?;
}
branch.name = dedup(
&all_virtual_branches
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>(),
&name,
);
project_repository
.git_repository
.reference(&branch.refname(), branch.head, false, "new vbranch")
.context("failed to create branch reference")?;
};
if let Some(upstream_branch_name) = branch_update.upstream {
@ -1217,7 +1248,7 @@ pub fn update_branch(
Some(target) => format!(
"refs/remotes/{}/{}",
target.branch.remote(),
name_to_branch(&upstream_branch_name)
slugify(upstream_branch_name)
)
.parse::<git::RemoteBranchName>()
.unwrap(),
@ -1264,14 +1295,11 @@ pub fn delete_branch(
// remove refs/butler reference
let repo = &project_repository.git_repository;
let branch_name = name_to_branch(&branch.name);
let ref_name = format!("refs/gitbutler/{}", branch_name);
println!("deleting ref: {}", ref_name);
if let Ok(mut reference) = repo.find_reference(&ref_name) {
println!("FOUND {}", ref_name);
let refname = branch.refname();
if let Ok(mut reference) = repo.find_reference(&refname) {
reference
.delete()
.context(format!("failed to delete {}", ref_name))?;
.context(format!("failed to delete {}", refname))?;
}
Ok(branch)
@ -1523,11 +1551,12 @@ fn get_applied_status(
if virtual_branches.is_empty() && !hunks_by_filepath.is_empty() {
// no virtual branches, but hunks: create default branch
virtual_branches =
vec![
create_virtual_branch(gb_repository, &BranchCreateRequest::default())
.context("failed to default branch")?,
];
virtual_branches = vec![create_virtual_branch(
gb_repository,
project_repository,
&BranchCreateRequest::default(),
)
.context("failed to default branch")?];
}
// align branch ownership to the real hunks:
@ -2047,12 +2076,6 @@ pub fn commit(
Ok(commit_oid)
}
pub fn name_to_branch(name: &str) -> String {
name.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect::<String>()
}
#[derive(Debug, thiserror::Error)]
pub enum CommitError {
#[error("will not commit conflicted files")]
@ -2099,7 +2122,7 @@ pub fn push(
Some(target) => format!(
"refs/remotes/{}/{}",
target.branch.remote(),
name_to_branch(&vbranch.name)
slugify(&vbranch.name)
)
.parse::<git::RemoteBranchName>()
.unwrap(),
@ -2111,11 +2134,8 @@ pub fn push(
.iter()
.map(RemoteBranchName::branch)
.collect::<Vec<_>>();
remote_branch.with_branch(&name_to_branch(&dedup_fmt(
&existing_branches,
remote_branch.branch(),
"-",
)))
remote_branch.with_branch(&dedup_fmt(&existing_branches, remote_branch.branch(), "-"))
};
project_repository.push(&vbranch.head, &remote_branch, with_force, credentials)?;

View File

@ -111,4 +111,12 @@ impl TestProject {
)
.expect("failed to commit")
}
pub fn references(&self) -> Vec<git::Reference> {
self.local_repository
.references()
.expect("failed to get references")
.collect::<Result<Vec<_>, _>>()
.expect("failed to read references")
}
}

View File

@ -35,10 +35,10 @@ impl Default for Test {
}
}
mod create_virtual_branch {
mod references {
use super::*;
mod name {
mod create_virtual_branch {
use super::*;
@ -47,6 +47,7 @@ mod create_virtual_branch {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
@ -65,6 +66,14 @@ mod create_virtual_branch {
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert_eq!(branches[0].name, "Virtual branch");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&"refs/gitbutler/virtual-branch".to_string()))
}
#[tokio::test]
@ -72,6 +81,7 @@ mod create_virtual_branch {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
@ -110,15 +120,19 @@ mod create_virtual_branch {
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "name");
assert_eq!(branches[1].id, branch2_id);
assert_eq!(branches[1].name, "name");
assert_eq!(branches[1].name, "name 1");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&"refs/gitbutler/name".to_string()));
assert!(refnames.contains(&"refs/gitbutler/name-1".to_string()));
}
}
}
mod update_virtual_branch {
use super::*;
mod name {
mod update_virtual_branch {
use super::*;
#[tokio::test]
@ -126,6 +140,7 @@ mod update_virtual_branch {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
@ -137,7 +152,13 @@ mod update_virtual_branch {
.unwrap();
let branch_id = controller
.create_virtual_branch(&project_id, &branch::BranchCreateRequest::default())
.create_virtual_branch(
&project_id,
&gitbutler::virtual_branches::branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
@ -157,6 +178,14 @@ mod update_virtual_branch {
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch_id);
assert_eq!(branches[0].name, "new name");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(!refnames.contains(&"refs/gitbutler/name".to_string()));
assert!(refnames.contains(&"refs/gitbutler/new-name".to_string()));
}
#[tokio::test]
@ -164,6 +193,7 @@ mod update_virtual_branch {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
@ -212,7 +242,226 @@ mod update_virtual_branch {
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "name");
assert_eq!(branches[1].id, branch2_id);
assert_eq!(branches[1].name, "name 1");
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&"refs/gitbutler/name".to_string()));
assert!(refnames.contains(&"refs/gitbutler/name-1".to_string()));
}
}
mod delete_virtual_branch {
use super::*;
#[tokio::test]
async fn simple() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(
&project_id,
&git::RemoteBranchName::from_str("refs/remotes/origin/master").unwrap(),
)
.unwrap();
let id = controller
.create_virtual_branch(
&project_id,
&gitbutler::virtual_branches::branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
controller
.delete_virtual_branch(&project_id, &id)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 0);
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(!refnames.contains(&"refs/gitbutler/name".to_string()));
}
}
mod push_virtual_branch {
use gitbutler::virtual_branches::branch::BranchUpdateRequest;
use super::*;
#[tokio::test]
async fn simple() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(
&project_id,
&git::RemoteBranchName::from_str("refs/remotes/origin/master").unwrap(),
)
.unwrap();
let branch1_id = controller
.create_virtual_branch(
&project_id,
&gitbutler::virtual_branches::branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch1_id, "test", None)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch1_id, false)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "name");
assert_eq!(
branches[0].upstream,
Some("refs/remotes/origin/name".parse().unwrap())
);
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&branches[0].upstream.clone().unwrap().to_string()));
}
#[tokio::test]
async fn duplicate_names() {
let Test {
project_id,
controller,
repository,
..
} = Test::default();
controller
.set_base_branch(
&project_id,
&git::RemoteBranchName::from_str("refs/remotes/origin/master").unwrap(),
)
.unwrap();
let branch1_id = {
// create and push branch with some work
let branch1_id = controller
.create_virtual_branch(
&project_id,
&gitbutler::virtual_branches::branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "content").unwrap();
controller
.create_commit(&project_id, &branch1_id, "test", None)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch1_id, false)
.await
.unwrap();
branch1_id
};
// rename first branch
controller
.update_virtual_branch(
&project_id,
BranchUpdateRequest {
id: branch1_id,
name: Some("updated name".to_string()),
..Default::default()
},
)
.await
.unwrap();
let branch2_id = {
// create another branch with first branch's old name and push it
let branch2_id = controller
.create_virtual_branch(
&project_id,
&gitbutler::virtual_branches::branch::BranchCreateRequest {
name: Some("name".to_string()),
..Default::default()
},
)
.await
.unwrap();
fs::write(repository.path().join("file.txt"), "updated content").unwrap();
controller
.create_commit(&project_id, &branch2_id, "test", None)
.await
.unwrap();
controller
.push_virtual_branch(&project_id, &branch2_id, false)
.await
.unwrap();
branch2_id
};
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 2);
// first branch is pushing to old ref remotely
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].name, "updated name");
assert_eq!(
branches[0].upstream,
Some("refs/remotes/origin/name".parse().unwrap())
);
// new branch is pushing to new ref remotely
assert_eq!(branches[1].id, branch2_id);
assert_eq!(branches[1].name, "name");
assert_eq!(
branches[1].upstream,
Some("refs/remotes/origin/name-1".parse().unwrap())
);
let refnames = repository
.references()
.into_iter()
.filter_map(|reference| reference.name().map(|name| name.to_string()))
.collect::<Vec<_>>();
assert!(refnames.contains(&branches[0].upstream.clone().unwrap().to_string()));
assert!(refnames.contains(&branches[1].upstream.clone().unwrap().to_string()));
}
}
}
@ -650,3 +899,271 @@ mod conflicts {
}
}
}
mod reset {
use super::*;
#[tokio::test]
async fn to_head() {
let Test {
repository,
project_id,
controller,
} = Test::default();
controller
.set_base_branch(
&project_id,
&git::RemoteBranchName::from_str("refs/remotes/origin/master").unwrap(),
)
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &Default::default())
.await
.unwrap();
let oid = {
fs::write(repository.path().join("file.txt"), "content").unwrap();
// commit changes
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
oid
};
{
// reset changes to head
controller
.reset_virtual_branch(&project_id, &branch1_id, oid)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
}
}
#[tokio::test]
async fn to_target() {
let Test {
repository,
project_id,
controller,
} = Test::default();
let base_branch = controller
.set_base_branch(
&project_id,
&git::RemoteBranchName::from_str("refs/remotes/origin/master").unwrap(),
)
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &Default::default())
.await
.unwrap();
{
fs::write(repository.path().join("file.txt"), "content").unwrap();
// commit changes
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
}
{
// reset changes to head
controller
.reset_virtual_branch(&project_id, &branch1_id, base_branch.base_sha)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 0);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
}
}
#[tokio::test]
async fn to_commit() {
let Test {
repository,
project_id,
controller,
} = Test::default();
controller
.set_base_branch(
&project_id,
&git::RemoteBranchName::from_str("refs/remotes/origin/master").unwrap(),
)
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &Default::default())
.await
.unwrap();
let first_commit_oid = {
// commit some changes
fs::write(repository.path().join("file.txt"), "content").unwrap();
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
oid
};
{
// commit some more
fs::write(repository.path().join("file.txt"), "more content").unwrap();
let second_commit_oid = controller
.create_commit(&project_id, &branch1_id, "commit", None)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 2);
assert_eq!(branches[0].commits[0].id, second_commit_oid);
assert_eq!(branches[0].commits[1].id, first_commit_oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"more content"
);
}
{
// reset changes to the first commit
controller
.reset_virtual_branch(&project_id, &branch1_id, first_commit_oid)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, first_commit_oid);
assert_eq!(branches[0].files.len(), 1);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"more content"
);
}
}
#[tokio::test]
async fn to_non_existing() {
let Test {
repository,
project_id,
controller,
} = Test::default();
controller
.set_base_branch(
&project_id,
&git::RemoteBranchName::from_str("refs/remotes/origin/master").unwrap(),
)
.unwrap();
let branch1_id = controller
.create_virtual_branch(&project_id, &Default::default())
.await
.unwrap();
{
fs::write(repository.path().join("file.txt"), "content").unwrap();
// commit changes
let oid = controller
.create_commit(&project_id, &branch1_id, "commit", None)
.await
.unwrap();
let branches = controller.list_virtual_branches(&project_id).await.unwrap();
assert_eq!(branches.len(), 1);
assert_eq!(branches[0].id, branch1_id);
assert_eq!(branches[0].commits.len(), 1);
assert_eq!(branches[0].commits[0].id, oid);
assert_eq!(branches[0].files.len(), 0);
assert_eq!(
fs::read_to_string(repository.path().join("file.txt")).unwrap(),
"content"
);
oid
};
assert!(matches!(
controller
.reset_virtual_branch(
&project_id,
&branch1_id,
"fe14df8c66b73c6276f7bb26102ad91da680afcb".parse().unwrap()
)
.await,
Err(ControllerError::Other(_))
));
}
}

View File

@ -26,6 +26,8 @@
import BaseBranchSelect from './BaseBranchSelect.svelte';
import { unsubscribe } from '$lib/utils';
import * as hotkeys from '$lib/hotkeys';
import { userStore } from '$lib/stores/user';
import type { GitHubIntegrationContext } from '$lib/github/types';
export let data: PageData;
let { projectId, project, cloud } = data;
@ -81,6 +83,24 @@
)
)
);
function getIntegrationContext(
remoteUrl: string,
githubAuthToken: string
): GitHubIntegrationContext | undefined {
if (!remoteUrl.includes('github')) return undefined;
const [owner, repo] = remoteUrl.split('.git')[0].split(/\/|:/).slice(-2);
return {
authToken: githubAuthToken,
owner,
repo
};
}
$: githubContext =
$baseBranchStore?.remoteUrl && $userStore?.github_access_token
? getIntegrationContext($baseBranchStore?.remoteUrl, $userStore?.github_access_token)
: undefined;
</script>
{#if $baseBranchesState.isLoading}
@ -98,6 +118,7 @@
bind:peekTrayExpanded
{cloud}
{projectId}
{githubContext}
/>
</div>
<Resizer
@ -151,6 +172,7 @@
baseBranchState={$baseBranchesState}
cloudEnabled={$project?.api?.sync || false}
{cloud}
{githubContext}
/>
</div>
<!-- <BottomPanel base={$baseBranchStore} {userSettings} /> -->

View File

@ -8,6 +8,7 @@
import type { LoadState } from '@square/svelte-store';
import { open } from '@tauri-apps/api/shell';
import { IconFile, IconTerminal, IconExternalLink } from '$lib/icons';
import type { GitHubIntegrationContext } from '$lib/github/types';
export let projectId: string;
export let projectPath: string;
@ -22,6 +23,8 @@
export let cloud: ReturnType<typeof getCloudApiClient>;
export let branchController: BranchController;
export let githubContext: GitHubIntegrationContext | undefined
let dragged: any;
let dropZone: HTMLDivElement;
let priorPosition = 0;
@ -100,6 +103,7 @@
{cloud}
{branchController}
branchCount={branches.filter((c) => c.active).length}
{githubContext}
/>
{/each}

View File

@ -65,6 +65,7 @@
export let branchController: BranchController;
export let maximized = false;
export let branchCount = 1;
export let githubContext: GitHubIntegrationContext | undefined;
const user = userStore;
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
@ -87,20 +88,6 @@
const dzType = 'text/hunk';
const laneWidthKey = 'laneWidth:';
function getIntegrationContext(): GitHubIntegrationContext | undefined {
const remoteUrl = base?.remoteUrl;
if (!remoteUrl) return undefined;
const [owner, repo] = remoteUrl.split('.git')[0].split(/\/|:/).slice(-2);
const authToken = $user?.github_access_token;
if (!authToken) return undefined;
return {
authToken,
owner,
repo
};
}
$: githubContext = getIntegrationContext();
$: pullRequestPromise =
githubContext && branch.upstream
? getPullRequestByBranch(githubContext, branch.upstream.split('/').slice(-1)[0])

View File

@ -9,6 +9,7 @@
import RemoteBranchPeek from './RemoteBranchPeek.svelte';
import Resizer from '$lib/components/Resizer.svelte';
import Lane from './BranchLane.svelte';
import type { GitHubIntegrationContext } from '$lib/github/types';
import type { getCloudApiClient } from '$lib/api/cloud/api';
export let item: Readable<RemoteBranch | Branch | BaseBranch | undefined> | undefined;
@ -20,6 +21,7 @@
export let projectId: string;
export let fullHeight = false;
export let disabled = false;
export let githubContext: GitHubIntegrationContext | undefined;
let viewport: HTMLElement;
@ -81,6 +83,7 @@
cloudEnabled={false}
projectPath=""
readonly={true}
{githubContext}
/>
{:else if $item instanceof BaseBranch}
<BaseBranchPeek {projectId} base={$item} {branchController} />

View File

@ -0,0 +1,159 @@
<script lang="ts">
import { Link } from '$lib/components';
import { IconGitBranch, IconRemote } from '$lib/icons';
import IconHelp from '$lib/icons/IconHelp.svelte';
import Scrollbar from '$lib/components/Scrollbar.svelte';
import Tooltip from '$lib/components/Tooltip/Tooltip.svelte';
import { IconTriangleDown, IconTriangleUp } from '$lib/icons';
import TimeAgo from '$lib/components/TimeAgo/TimeAgo.svelte';
import { accordion } from './accordion';
import type { CustomStore, RemoteBranch, BaseBranch, Branch } from '$lib/vbranches/types';
import type { Readable } from '@square/svelte-store';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
selection: {
branch: RemoteBranch;
i: number;
offset: number;
};
}>();
export let remoteBranchStore: CustomStore<RemoteBranch[] | undefined>;
let rbViewport: HTMLElement;
let rbContents: HTMLElement;
let rbSection: HTMLElement;
export let peekTrayExpanded = false;
export let selectedItem: Readable<Branch | RemoteBranch | BaseBranch | undefined> | undefined;
$: remoteBranchesState = remoteBranchStore?.state;
let open = false;
function select(branch: RemoteBranch, i: number) {
const element = rbContents.children[i] as HTMLDivElement;
const offset = element.offsetTop + rbSection.offsetTop - rbViewport.scrollTop;
dispatch('selection', { branch, i, offset });
}
</script>
<div
class="bg-color-4 border-color-4 flex items-center justify-between border-b border-t px-2 py-1 pr-1"
>
<div class="flex flex-row place-items-center space-x-2">
<div class="text-color-2 font-bold">Remote Branches</div>
<a
target="_blank"
rel="noreferrer"
href="https://docs.gitbutler.com/features/virtual-branches/remote-branches"
>
<IconHelp class="text-color-3 h-3 w-3" />
</a>
</div>
<div class="flex h-4 w-4 justify-around">
<button class="h-full w-full" on:click={() => (open = !open)}>
{#if open}
<IconTriangleUp />
{:else}
<IconTriangleDown />
{/if}
</button>
</div>
</div>
<div bind:this={rbSection} use:accordion={open} class="border-color-5 relative flex-grow border-b">
<div
bind:this={rbViewport}
on:scroll
class="hide-native-scrollbar flex max-h-full flex-grow flex-col overflow-y-scroll overscroll-none"
>
<div bind:this={rbContents}>
{#if $remoteBranchesState.isLoading}
<div class="px-2 py-1">loading...</div>
{:else if $remoteBranchesState.isError}
<div class="px-2 py-1">Something went wrong</div>
{:else if !$remoteBranchStore || $remoteBranchStore.length == 0}
<div class="p-4">
<p class="text-color-3 mb-2">
There are no local or remote Git branches that can be imported as virtual branches
</p>
<Link
target="_blank"
rel="noreferrer"
href="https://docs.gitbutler.com/features/virtual-branches/remote-branches"
>
Learn more
</Link>
</div>
{:else if $remoteBranchStore}
{#each $remoteBranchStore as branch, i}
<div
role="button"
tabindex="0"
on:click={() => select(branch, i)}
on:keypress={() => select(branch, i)}
class:bg-color-4={$selectedItem == branch && peekTrayExpanded}
class="border-color-4 flex flex-col justify-between gap-1 border-b px-2 py-1 pt-2 -outline-offset-2 outline-blue-200 last:border-b focus:outline-2"
>
<div class="flex flex-row items-center gap-x-2 pr-1">
<div class="text-color-3">
{#if branch.name.match('refs/remotes')}
<Tooltip
label="This is a remote branch that you don't have a virtual branch tracking yet"
>
<IconRemote class="h-4 w-4" />
</Tooltip>
{:else}
<Tooltip label="This is a local branch that is not a virtual branch yet">
<IconGitBranch class="h-4 w-4" />
</Tooltip>
{/if}
</div>
<div class="text-color-2 flex-grow truncate" title={branch.name}>
{branch.name
.replace('refs/remotes/', '')
.replace('origin/', '')
.replace('refs/heads/', '')}
</div>
</div>
<div class="flex flex-row justify-between space-x-2 rounded p-1 pr-1">
<div class="text-color-4 flex-grow-0 text-sm">
<TimeAgo date={branch.lastCommitTs()} />
</div>
<div class="flex flex-grow-0 flex-row space-x-2">
<Tooltip
label="This branch has {branch.ahead()} commits not on your base branch and your base has {branch.behind} commits not on this branch yet"
>
<div class="bg-color-3 text-color-3 rounded-lg px-2 text-sm">
{branch.ahead()} / {branch.behind}
</div>
</Tooltip>
{#await branch.isMergeable then isMergeable}
{#if !isMergeable}
<div class="font-bold text-red-500" title="Can't be merged">!</div>
{/if}
{/await}
</div>
<div
class="isolate flex flex-grow justify-end -space-x-2 overflow-hidden transition duration-300 ease-in-out hover:space-x-1 hover:transition hover:ease-in"
>
{#each branch.authors() as author}
<img
class="relative z-30 inline-block h-4 w-4 rounded-full ring-1 ring-white dark:ring-black"
title="Gravatar for {author.email}"
alt="Gravatar for {author.email}"
srcset="{author.gravatarUrl} 2x"
width="100"
height="100"
on:error
/>
{/each}
</div>
</div>
</div>
{/each}
{/if}
</div>
</div>
<Scrollbar viewport={rbViewport} contents={rbContents} width="0.5rem" />
</div>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import { Link } from '$lib/components';
import { Branch, BaseBranch, RemoteBranch, type CustomStore } from '$lib/vbranches/types';
import { IconBranch, IconGitBranch, IconRemote } from '$lib/icons';
import { IconBranch } from '$lib/icons';
import { IconTriangleDown, IconTriangleUp } from '$lib/icons';
import { accordion } from './accordion';
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/userSettings';
@ -9,7 +8,6 @@
import type { BranchController } from '$lib/vbranches/branchController';
import Tooltip from '$lib/components/Tooltip/Tooltip.svelte';
import Scrollbar from '$lib/components/Scrollbar.svelte';
import IconHelp from '$lib/icons/IconHelp.svelte';
import { derived, get, type Readable } from '@square/svelte-store';
import PeekTray from './PeekTray.svelte';
import IconRefresh from '$lib/icons/IconRefresh.svelte';
@ -23,6 +21,8 @@
import IconChevronRightSmall from '$lib/icons/IconChevronRightSmall.svelte';
import { slide } from 'svelte/transition';
import { computedAddedRemoved } from '$lib/vbranches/fileStatus';
import RemoteBranches from './RemoteBranches.svelte';
import type { GitHubIntegrationContext } from '$lib/github/types';
export let branchesWithContentStore: CustomStore<Branch[] | undefined>;
export let remoteBranchStore: CustomStore<RemoteBranch[] | undefined>;
@ -32,29 +32,29 @@
export let projectId: string;
export let cloud: ReturnType<typeof getCloudApiClient>;
export let peekTrayExpanded = false;
export let githubContext: GitHubIntegrationContext | undefined;
$: branchesState = branchesWithContentStore?.state;
$: remoteBranchesState = remoteBranchStore?.state;
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
let yourBranchesOpen = true;
let remoteBranchesOpen = true;
let applyConflictedModal: Modal;
let vbViewport: HTMLElement;
let vbContents: HTMLElement;
let rbViewport: HTMLElement;
let rbContents: HTMLElement;
let rbSection: HTMLElement;
let baseContents: HTMLElement;
let selectedItem: Readable<Branch | RemoteBranch | BaseBranch | undefined> | undefined;
let overlayOffsetTop = 0;
let fetching = false;
function select(detail: Branch | RemoteBranch | BaseBranch | undefined, i: number): void {
function select(
detail: Branch | RemoteBranch | BaseBranch | undefined,
i: number,
offset?: number
): void {
if (peekTrayExpanded && selectedItem && detail == get(selectedItem)) {
peekTrayExpanded = false;
return;
@ -69,8 +69,7 @@
selectedItem = derived(remoteBranchStore, (branches) =>
branches?.find((remoteBranch) => remoteBranch.sha == detail.sha)
);
const element = rbContents.children[i] as HTMLDivElement;
overlayOffsetTop = element.offsetTop + rbSection.offsetTop - rbViewport.scrollTop;
overlayOffsetTop = offset || overlayOffsetTop;
} else if (detail instanceof BaseBranch) {
selectedItem = baseBranchStore;
overlayOffsetTop = baseContents.offsetTop;
@ -118,6 +117,7 @@
disabled={peekTransitionsDisabled}
{cloud}
{projectId}
{githubContext}
/>
<div
class="bg-color-5 border-color-4 z-30 flex w-80 shrink-0 flex-col border-r"
@ -308,130 +308,13 @@
/>
<!-- Remote branches -->
<div
class="bg-color-4 border-color-4 flex items-center justify-between border-b border-t px-2 py-1 pr-1"
>
<div class="flex flex-row place-items-center space-x-2">
<div class="text-color-2 font-bold">Remote Branches</div>
<a
target="_blank"
rel="noreferrer"
href="https://docs.gitbutler.com/features/virtual-branches/remote-branches"
>
<IconHelp class="text-color-3 h-3 w-3" />
</a>
</div>
<div class="flex h-4 w-4 justify-around">
<button class="h-full w-full" on:click={() => (remoteBranchesOpen = !remoteBranchesOpen)}>
{#if remoteBranchesOpen}
<IconTriangleUp />
{:else}
<IconTriangleDown />
{/if}
</button>
</div>
</div>
<div
bind:this={rbSection}
use:accordion={remoteBranchesOpen}
class="border-color-5 relative flex-grow border-b"
>
<div
bind:this={rbViewport}
on:scroll={onScroll}
class="hide-native-scrollbar flex max-h-full flex-grow flex-col overflow-y-scroll overscroll-none"
>
<div bind:this={rbContents}>
{#if $remoteBranchesState.isLoading}
<div class="px-2 py-1">loading...</div>
{:else if $remoteBranchesState.isError}
<div class="px-2 py-1">Something went wrong</div>
{:else if !$remoteBranchStore || $remoteBranchStore.length == 0}
<div class="p-4">
<p class="text-color-3 mb-2">
There are no local or remote Git branches that can be imported as virtual branches
</p>
<Link
target="_blank"
rel="noreferrer"
href="https://docs.gitbutler.com/features/virtual-branches/remote-branches"
>
Learn more
</Link>
</div>
{:else if $remoteBranchStore}
{#each $remoteBranchStore as branch, i}
<div
role="button"
tabindex="0"
on:click={() => select(branch, i)}
on:keypress={() => select(branch, i)}
class:bg-color-4={$selectedItem == branch && peekTrayExpanded}
class="border-color-4 flex flex-col justify-between gap-1 border-b px-2 py-1 pt-2 -outline-offset-2 outline-blue-200 last:border-b focus:outline-2"
>
<div class="flex flex-row items-center gap-x-2 pr-1">
<div class="text-color-3">
{#if branch.name.match('refs/remotes')}
<Tooltip
label="This is a remote branch that you don't have a virtual branch tracking yet"
>
<IconRemote class="h-4 w-4" />
</Tooltip>
{:else}
<Tooltip label="This is a local branch that is not a virtual branch yet">
<IconGitBranch class="h-4 w-4" />
</Tooltip>
{/if}
</div>
<div class="text-color-2 flex-grow truncate" title={branch.name}>
{branch.name
.replace('refs/remotes/', '')
.replace('origin/', '')
.replace('refs/heads/', '')}
</div>
</div>
<div class="flex flex-row justify-between space-x-2 rounded p-1 pr-1">
<div class="text-color-4 flex-grow-0 text-sm">
<TimeAgo date={branch.lastCommitTs()} />
</div>
<div class="flex flex-grow-0 flex-row space-x-2">
<Tooltip
label="This branch has {branch.ahead()} commits not on your base branch and your base has {branch.behind} commits not on this branch yet"
>
<div class="bg-color-3 text-color-3 rounded-lg px-2 text-sm">
{branch.ahead()} / {branch.behind}
</div>
</Tooltip>
{#await branch.isMergeable then isMergeable}
{#if !isMergeable}
<div class="font-bold text-red-500" title="Can't be merged">!</div>
{/if}
{/await}
</div>
<div
class="isolate flex flex-grow justify-end -space-x-2 overflow-hidden transition duration-300 ease-in-out hover:space-x-1 hover:transition hover:ease-in"
>
{#each branch.authors() as author}
<img
class="relative z-30 inline-block h-4 w-4 rounded-full ring-1 ring-white dark:ring-black"
title="Gravatar for {author.email}"
alt="Gravatar for {author.email}"
srcset="{author.gravatarUrl} 2x"
width="100"
height="100"
on:error
/>
{/each}
</div>
</div>
</div>
{/each}
{/if}
</div>
</div>
<Scrollbar viewport={rbViewport} contents={rbContents} width="0.5rem" />
</div>
<RemoteBranches
on:scroll={onScroll}
on:selection={(e) => select(e.detail.branch, e.detail.i, e.detail.offset)}
{remoteBranchStore}
{peekTrayExpanded}
{selectedItem}
></RemoteBranches>
</div>
<Modal width="small" bind:this={applyConflictedModal}>