merge upstream

This commit is contained in:
Nikita Galaiko 2024-02-27 10:21:12 +01:00
commit f147f12db0
72 changed files with 1633 additions and 756 deletions

View File

@ -5,7 +5,6 @@
"rust-lang.rust-analyzer", "rust-lang.rust-analyzer",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"ZixuanChen.vitest-explorer",
"EditorConfig.EditorConfig" "EditorConfig.EditorConfig"
] ]
} }

View File

@ -59,6 +59,7 @@ impl Default for Options {
pub fn workdir( pub fn workdir(
repository: &Repository, repository: &Repository,
commit_oid: &git::Oid, commit_oid: &git::Oid,
context_lines: u32,
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> { ) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
let commit = repository let commit = repository
.find_commit(*commit_oid) .find_commit(*commit_oid)
@ -72,7 +73,7 @@ pub fn workdir(
.show_binary(true) .show_binary(true)
.show_untracked_content(true) .show_untracked_content(true)
.ignore_submodules(true) .ignore_submodules(true)
.context_lines(0); .context_lines(context_lines);
let diff = repository.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?; let diff = repository.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?;
@ -83,6 +84,7 @@ pub fn trees(
repository: &Repository, repository: &Repository,
old_tree: &git::Tree, old_tree: &git::Tree,
new_tree: &git::Tree, new_tree: &git::Tree,
context_lines: u32,
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> { ) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
let mut diff_opts = git2::DiffOptions::new(); let mut diff_opts = git2::DiffOptions::new();
diff_opts diff_opts
@ -90,7 +92,7 @@ pub fn trees(
.include_untracked(true) .include_untracked(true)
.show_binary(true) .show_binary(true)
.ignore_submodules(true) .ignore_submodules(true)
.context_lines(0) .context_lines(context_lines)
.show_untracked_content(true); .show_untracked_content(true);
let diff = let diff =
@ -340,7 +342,7 @@ mod tests {
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
let diff = workdir(&repository, &head_commit_id).unwrap(); let diff = workdir(&repository, &head_commit_id, 0).unwrap();
assert_eq!(diff.len(), 1); assert_eq!(diff.len(), 1);
assert_eq!( assert_eq!(
diff[&path::PathBuf::from("file")], diff[&path::PathBuf::from("file")],
@ -363,7 +365,7 @@ mod tests {
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
let diff = workdir(&repository, &head_commit_id).unwrap(); let diff = workdir(&repository, &head_commit_id, 0).unwrap();
assert_eq!(diff.len(), 1); assert_eq!(diff.len(), 1);
assert_eq!( assert_eq!(
diff[&path::PathBuf::from("first")], diff[&path::PathBuf::from("first")],
@ -387,7 +389,7 @@ mod tests {
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
let diff = workdir(&repository, &head_commit_id).unwrap(); let diff = workdir(&repository, &head_commit_id, 0).unwrap();
assert_eq!(diff.len(), 2); assert_eq!(diff.len(), 2);
assert_eq!( assert_eq!(
diff[&path::PathBuf::from("first")], diff[&path::PathBuf::from("first")],
@ -431,7 +433,7 @@ mod tests {
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
let diff = workdir(&repository, &head_commit_id).unwrap(); let diff = workdir(&repository, &head_commit_id, 0).unwrap();
assert_eq!( assert_eq!(
diff[&path::PathBuf::from("image")], diff[&path::PathBuf::from("image")],
vec![Hunk { vec![Hunk {
@ -474,7 +476,7 @@ mod tests {
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id(); let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
let diff = workdir(&repository, &head_commit_id).unwrap(); let diff = workdir(&repository, &head_commit_id, 0).unwrap();
assert_eq!( assert_eq!(
diff[&path::PathBuf::from("file")], diff[&path::PathBuf::from("file")],
vec![Hunk { vec![Hunk {

View File

@ -365,6 +365,13 @@ impl Repository {
if self.project.omit_certificate_check.unwrap_or(false) { if self.project.omit_certificate_check.unwrap_or(false) {
cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk)); cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk));
} }
cbs.push_update_reference(|_reference: &str, status: Option<&str>| {
if let Some(status) = status {
return Err(git2::Error::from_str(status));
};
Ok(())
});
match remote.push( match remote.push(
&[refspec.as_str()], &[refspec.as_str()],
Some(&mut git2::PushOptions::new().remote_callbacks(cbs)), Some(&mut git2::PushOptions::new().remote_callbacks(cbs)),

View File

@ -70,6 +70,10 @@ impl From<controller::AddError> for Error {
code: Code::Projects, code: Code::Projects,
message: "Path not found".to_string(), message: "Path not found".to_string(),
}, },
controller::AddError::SubmodulesNotSupported => Error::UserError {
code: Code::Projects,
message: "Repositories with git submodules are not supported".to_string(),
},
controller::AddError::User(error) => error.into(), controller::AddError::User(error) => error.into(),
controller::AddError::Other(error) => { controller::AddError::Other(error) => {
tracing::error!(?error, "failed to add project"); tracing::error!(?error, "failed to add project");

View File

@ -66,6 +66,10 @@ impl Controller {
return Err(AddError::NotAGitRepository); return Err(AddError::NotAGitRepository);
}; };
if path.join(".gitmodules").exists() {
return Err(AddError::SubmodulesNotSupported);
}
let id = uuid::Uuid::new_v4().to_string(); let id = uuid::Uuid::new_v4().to_string();
// title is the base name of the file // title is the base name of the file
@ -79,6 +83,7 @@ impl Controller {
title, title,
path: path.to_path_buf(), path: path.to_path_buf(),
api: None, api: None,
use_diff_context: Some(true),
..Default::default() ..Default::default()
}; };
@ -201,6 +206,10 @@ impl Controller {
tracing::error!(project_id = %id, ?error, "failed to remove project data",); tracing::error!(project_id = %id, ?error, "failed to remove project data",);
} }
if let Err(error) = std::fs::remove_file(project.path.join(".git/gitbutler.json")) {
tracing::error!(project_id = %project.id, ?error, "failed to remove .git/gitbutler.json data",);
}
Ok(()) Ok(())
} }
} }
@ -253,6 +262,8 @@ pub enum AddError {
PathNotFound, PathNotFound,
#[error("project already exists")] #[error("project already exists")]
AlreadyExists, AlreadyExists,
#[error("submodules not supported")]
SubmodulesNotSupported,
#[error(transparent)] #[error(transparent)]
User(#[from] users::GetError), User(#[from] users::GetError),
#[error(transparent)] #[error(transparent)]

View File

@ -78,6 +78,8 @@ pub struct Project {
pub project_data_last_fetch: Option<FetchResult>, pub project_data_last_fetch: Option<FetchResult>,
#[serde(default)] #[serde(default)]
pub omit_certificate_check: Option<bool>, pub omit_certificate_check: Option<bool>,
#[serde(default)]
pub use_diff_context: Option<bool>,
} }
impl AsRef<Project> for Project { impl AsRef<Project> for Project {

View File

@ -47,6 +47,7 @@ pub struct UpdateRequest {
pub gitbutler_code_push_state: Option<project::CodePushState>, pub gitbutler_code_push_state: Option<project::CodePushState>,
pub project_data_last_fetched: Option<project::FetchResult>, pub project_data_last_fetched: Option<project::FetchResult>,
pub omit_certificate_check: Option<bool>, pub omit_certificate_check: Option<bool>,
pub use_diff_context: Option<bool>,
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -139,6 +140,10 @@ impl Storage {
project.omit_certificate_check = Some(omit_certificate_check); project.omit_certificate_check = Some(omit_certificate_check);
} }
if let Some(use_diff_context) = update_request.use_diff_context {
project.use_diff_context = Some(use_diff_context);
}
self.storage self.storage
.write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?; .write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?;

View File

@ -135,7 +135,12 @@ pub fn set_base_branch(
// if there are any commits on the head branch or uncommitted changes in the working directory, we need to // if there are any commits on the head branch or uncommitted changes in the working directory, we need to
// put them into a virtual branch // put them into a virtual branch
let wd_diff = diff::workdir(repo, &current_head_commit.id())?; let use_context = project_repository
.project()
.use_diff_context
.unwrap_or(false);
let context_lines = if use_context { 3_u32 } else { 0_u32 };
let wd_diff = diff::workdir(repo, &current_head_commit.id(), context_lines)?;
if !wd_diff.is_empty() || current_head_commit.id() != target.sha { if !wd_diff.is_empty() || current_head_commit.id() != target.sha {
let hunks_by_filepath = let hunks_by_filepath =
super::virtual_hunks_by_filepath(&project_repository.project().path, &wd_diff); super::virtual_hunks_by_filepath(&project_repository.project().path, &wd_diff);
@ -308,6 +313,12 @@ pub fn update_base_branch(
let branch_writer = let branch_writer =
branch::Writer::new(gb_repository).context("failed to create branch writer")?; branch::Writer::new(gb_repository).context("failed to create branch writer")?;
let use_context = project_repository
.project()
.use_diff_context
.unwrap_or(false);
let context_lines = if use_context { 3_u32 } else { 0_u32 };
// try to update every branch // try to update every branch
let updated_vbranches = super::get_status_by_branch(gb_repository, project_repository)? let updated_vbranches = super::get_status_by_branch(gb_repository, project_repository)?
.into_iter() .into_iter()
@ -341,6 +352,7 @@ pub fn update_base_branch(
&project_repository.git_repository, &project_repository.git_repository,
&branch_head_tree, &branch_head_tree,
&branch_tree, &branch_tree,
context_lines,
)?; )?;
if non_commited_files.is_empty() { if non_commited_files.is_empty() {
// if there are no commited files, then the branch is fully merged // if there are no commited files, then the branch is fully merged

View File

@ -12,7 +12,7 @@ use crate::{
}; };
use super::{ use super::{
branch::BranchId, branch::{self, BranchId},
controller::{Controller, ControllerError}, controller::{Controller, ControllerError},
BaseBranch, RemoteBranchFile, BaseBranch, RemoteBranchFile,
}; };
@ -80,11 +80,23 @@ pub async fn list_virtual_branches(
code: Code::Validation, code: Code::Validation,
message: "Malformed project id".to_string(), message: "Malformed project id".to_string(),
})?; })?;
let branches = handle let (branches, uses_diff_context) = handle
.state::<Controller>() .state::<Controller>()
.list_virtual_branches(&project_id) .list_virtual_branches(&project_id)
.await?; .await?;
// Migration: If use_diff_context is not already set and if there are no vbranches, set use_diff_context to true
if !uses_diff_context && branches.is_empty() {
let _ = handle
.state::<projects::Controller>()
.update(&projects::UpdateRequest {
id: project_id,
use_diff_context: Some(true),
..Default::default()
})
.await;
}
let proxy = handle.state::<assets::Proxy>(); let proxy = handle.state::<assets::Proxy>();
let branches = proxy.proxy_virtual_branches(branches).await; let branches = proxy.proxy_virtual_branches(branches).await;
Ok(branches) Ok(branches)

View File

@ -124,7 +124,8 @@ impl Controller {
pub async fn list_virtual_branches( pub async fn list_virtual_branches(
&self, &self,
project_id: &ProjectId, project_id: &ProjectId,
) -> Result<Vec<super::VirtualBranch>, ControllerError<errors::ListVirtualBranchesError>> { ) -> Result<(Vec<super::VirtualBranch>, bool), ControllerError<errors::ListVirtualBranchesError>>
{
self.inner(project_id) self.inner(project_id)
.await .await
.list_virtual_branches(project_id) .list_virtual_branches(project_id)
@ -493,7 +494,8 @@ impl ControllerInner {
pub async fn list_virtual_branches( pub async fn list_virtual_branches(
&self, &self,
project_id: &ProjectId, project_id: &ProjectId,
) -> Result<Vec<super::VirtualBranch>, ControllerError<errors::ListVirtualBranchesError>> { ) -> Result<(Vec<super::VirtualBranch>, bool), ControllerError<errors::ListVirtualBranchesError>>
{
let _permit = self.semaphore.acquire().await; let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |gb_repository, project_repository, _| { self.with_verify_branch(project_id, |gb_repository, project_repository, _| {
@ -570,8 +572,16 @@ impl ControllerInner {
) -> Result<Vec<RemoteBranchFile>, Error> { ) -> Result<Vec<RemoteBranchFile>, Error> {
let project = self.projects.get(project_id)?; let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?; let project_repository = project_repository::Repository::open(&project)?;
let use_context = project_repository
super::list_remote_commit_files(&project_repository.git_repository, commit_oid) .project()
.use_diff_context
.unwrap_or(false);
let context_lines = if use_context { 3_u32 } else { 0_u32 };
super::list_remote_commit_files(
&project_repository.git_repository,
commit_oid,
context_lines,
)
.map_err(Into::into) .map_err(Into::into)
} }

View File

@ -19,6 +19,7 @@ pub struct RemoteBranchFile {
pub fn list_remote_commit_files( pub fn list_remote_commit_files(
repository: &git::Repository, repository: &git::Repository,
commit_oid: git::Oid, commit_oid: git::Oid,
context_lines: u32,
) -> Result<Vec<RemoteBranchFile>, errors::ListRemoteCommitFilesError> { ) -> Result<Vec<RemoteBranchFile>, errors::ListRemoteCommitFilesError> {
let commit = match repository.find_commit(commit_oid) { let commit = match repository.find_commit(commit_oid) {
Ok(commit) => Ok(commit), Ok(commit) => Ok(commit),
@ -35,7 +36,7 @@ pub fn list_remote_commit_files(
let parent = commit.parent(0).context("failed to get parent commit")?; let parent = commit.parent(0).context("failed to get parent commit")?;
let commit_tree = commit.tree().context("failed to get commit tree")?; let commit_tree = commit.tree().context("failed to get commit tree")?;
let parent_tree = parent.tree().context("failed to get parent tree")?; let parent_tree = parent.tree().context("failed to get parent tree")?;
let diff = diff::trees(repository, &parent_tree, &commit_tree)?; let diff = diff::trees(repository, &parent_tree, &commit_tree, context_lines)?;
let files = diff let files = diff
.into_iter() .into_iter()

View File

@ -81,7 +81,7 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
"line0\nline1\nline2\nline3\nline4\n", "line0\nline1\nline2\nline3\nline4\n",
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches[0]; let branch = &branches[0];
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
assert_eq!(branch.commits.len(), 0); assert_eq!(branch.commits.len(), 0);
@ -99,7 +99,7 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
)?; )?;
// status (no files) // status (no files)
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches[0]; let branch = &branches[0];
assert_eq!(branch.files.len(), 0); assert_eq!(branch.files.len(), 0);
assert_eq!(branch.commits.len(), 1); assert_eq!(branch.commits.len(), 1);
@ -110,7 +110,7 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
)?; )?;
// should have just the last change now, the other line is committed // should have just the last change now, the other line is committed
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches[0]; let branch = &branches[0];
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
assert_eq!(branch.commits.len(), 1); assert_eq!(branch.commits.len(), 1);
@ -170,7 +170,7 @@ fn test_signed_commit() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository).unwrap(); let (branches, _) = list_virtual_branches(&gb_repository, &project_repository).unwrap();
let commit_id = &branches[0].commits[0].id; let commit_id = &branches[0].commits[0].id;
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?; let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
// check the raw_header contains the string "SSH SIGNATURE" // check the raw_header contains the string "SSH SIGNATURE"
@ -235,7 +235,7 @@ fn test_track_binary_files() -> Result<()> {
let mut file = fs::File::create(std::path::Path::new(&project.path).join("image.bin"))?; let mut file = fs::File::create(std::path::Path::new(&project.path).join("image.bin"))?;
file.write_all(&image_data)?; file.write_all(&image_data)?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches[0]; let branch = &branches[0];
assert_eq!(branch.files.len(), 2); assert_eq!(branch.files.len(), 2);
let img_file = &branch let img_file = &branch
@ -262,7 +262,7 @@ fn test_track_binary_files() -> Result<()> {
)?; )?;
// status (no files) // status (no files)
let branches = list_virtual_branches(&gb_repository, &project_repository).unwrap(); let (branches, _) = list_virtual_branches(&gb_repository, &project_repository).unwrap();
let commit_id = &branches[0].commits[0].id; let commit_id = &branches[0].commits[0].id;
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?; let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
let tree = commit_obj.tree()?; let tree = commit_obj.tree()?;
@ -291,7 +291,7 @@ fn test_track_binary_files() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository).unwrap(); let (branches, _) = list_virtual_branches(&gb_repository, &project_repository).unwrap();
let commit_id = &branches[0].commits[0].id; let commit_id = &branches[0].commits[0].id;
// get tree from commit_id // get tree from commit_id
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?; let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
@ -621,7 +621,7 @@ fn test_updated_ownership_should_bubble_up() -> Result<()> {
)?; )?;
get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status"); get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status");
let files = branch_reader.read(&branch1_id)?.ownership.files; let files = branch_reader.read(&branch1_id)?.ownership.files;
assert_eq!(files, vec!["test.txt:14-15,1-2".parse()?]); assert_eq!(files, vec!["test.txt:11-15,1-5".parse()?]);
assert_eq!( assert_eq!(
files[0].hunks[0].timestam_ms(), files[0].hunks[0].timestam_ms(),
files[0].hunks[1].timestam_ms(), files[0].hunks[1].timestam_ms(),
@ -641,7 +641,7 @@ fn test_updated_ownership_should_bubble_up() -> Result<()> {
let files1 = branch_reader.read(&branch1_id)?.ownership.files; let files1 = branch_reader.read(&branch1_id)?.ownership.files;
assert_eq!( assert_eq!(
files1, files1,
vec!["test2.txt:1-2".parse()?, "test.txt:14-15,1-2".parse()?] vec!["test2.txt:1-2".parse()?, "test.txt:11-15,1-5".parse()?]
); );
assert_ne!( assert_ne!(
@ -667,7 +667,7 @@ fn test_updated_ownership_should_bubble_up() -> Result<()> {
let files2 = branch_reader.read(&branch1_id)?.ownership.files; let files2 = branch_reader.read(&branch1_id)?.ownership.files;
assert_eq!( assert_eq!(
files2, files2,
vec!["test.txt:1-3,14-15".parse()?, "test2.txt:1-2".parse()?,] vec!["test.txt:1-6,11-15".parse()?, "test2.txt:1-2".parse()?,]
); );
assert_ne!( assert_ne!(
@ -736,12 +736,12 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
let branch_writer = branch::Writer::new(&gb_repository)?; let branch_writer = branch::Writer::new(&gb_repository)?;
let mut branch2 = branch_reader.read(&branch2_id)?; let mut branch2 = branch_reader.read(&branch2_id)?;
branch2.ownership = Ownership { branch2.ownership = Ownership {
files: vec!["test.txt:1-2".parse()?], files: vec!["test.txt:1-5".parse()?],
}; };
branch_writer.write(&mut branch2)?; branch_writer.write(&mut branch2)?;
let mut branch1 = branch_reader.read(&branch1_id)?; let mut branch1 = branch_reader.read(&branch1_id)?;
branch1.ownership = Ownership { branch1.ownership = Ownership {
files: vec!["test.txt:14-15".parse()?], files: vec!["test.txt:11-15".parse()?],
}; };
branch_writer.write(&mut branch1)?; branch_writer.write(&mut branch1)?;
@ -765,7 +765,7 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
&project_repository, &project_repository,
branch::BranchUpdateRequest { branch::BranchUpdateRequest {
id: branch3_id, id: branch3_id,
ownership: Some("test.txt:1-2,14-15".parse()?), ownership: Some("test.txt:1-5,11-15".parse()?),
..Default::default() ..Default::default()
}, },
)?; )?;
@ -788,11 +788,11 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
); );
assert_eq!( assert_eq!(
files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][0].diff, files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][0].diff,
"@@ -0,0 +1 @@\n+line0\n" "@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
); );
assert_eq!( assert_eq!(
files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][1].diff, files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][1].diff,
"@@ -12,0 +14 @@ line12\n+line13\n" "@@ -10,3 +11,4 @@ line9\n line10\n line11\n line12\n+line13\n"
); );
Ok(()) Ok(())
} }
@ -848,7 +848,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
&project_repository, &project_repository,
branch::BranchUpdateRequest { branch::BranchUpdateRequest {
id: branch2_id, id: branch2_id,
ownership: Some("test.txt:1-2".parse()?), ownership: Some("test.txt:1-5".parse()?),
..Default::default() ..Default::default()
}, },
)?; )?;
@ -869,7 +869,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
); );
assert_eq!( assert_eq!(
files_by_branch_id[&branch1_id][std::path::Path::new("test.txt")][0].diff, files_by_branch_id[&branch1_id][std::path::Path::new("test.txt")][0].diff,
"@@ -13,0 +15 @@ line13\n+line14\n" "@@ -11,3 +12,4 @@ line10\n line11\n line12\n line13\n+line14\n"
); );
assert_eq!(files_by_branch_id[&branch2_id].len(), 1); assert_eq!(files_by_branch_id[&branch2_id].len(), 1);
@ -879,7 +879,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
); );
assert_eq!( assert_eq!(
files_by_branch_id[&branch2_id][std::path::Path::new("test.txt")][0].diff, files_by_branch_id[&branch2_id][std::path::Path::new("test.txt")][0].diff,
"@@ -0,0 +1 @@\n+line0\n" "@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
); );
Ok(()) Ok(())
@ -915,11 +915,7 @@ fn test_add_new_hunk_to_the_end() -> Result<()> {
get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status"); get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status");
assert_eq!( assert_eq!(
statuses[0].1[std::path::Path::new("test.txt")][0].diff, statuses[0].1[std::path::Path::new("test.txt")][0].diff,
"@@ -14 +13,0 @@ line13\n-line13\n" "@@ -11,5 +11,5 @@ line10\n line11\n line12\n line13\n-line13\n line14\n+line15\n"
);
assert_eq!(
statuses[0].1[std::path::Path::new("test.txt")][1].diff,
"@@ -15,0 +15 @@ line14\n+line15\n"
); );
std::fs::write( std::fs::write(
@ -932,15 +928,11 @@ fn test_add_new_hunk_to_the_end() -> Result<()> {
assert_eq!( assert_eq!(
statuses[0].1[std::path::Path::new("test.txt")][0].diff, statuses[0].1[std::path::Path::new("test.txt")][0].diff,
"@@ -15,0 +16 @@ line14\n+line15\n" "@@ -11,5 +12,5 @@ line10\n line11\n line12\n line13\n-line13\n line14\n+line15\n"
); );
assert_eq!( assert_eq!(
statuses[0].1[std::path::Path::new("test.txt")][1].diff, statuses[0].1[std::path::Path::new("test.txt")][1].diff,
"@@ -0,0 +1 @@\n+line0\n" "@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
);
assert_eq!(
statuses[0].1[std::path::Path::new("test.txt")][2].diff,
"@@ -14 +14,0 @@ line13\n-line13\n"
); );
Ok(()) Ok(())
@ -1040,7 +1032,7 @@ fn test_merge_vbranch_upstream_clean_rebase() -> Result<()> {
.context("failed to write target branch after push")?; .context("failed to write target branch after push")?;
// create the branch // create the branch
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches[0]; let branch1 = &branches[0];
assert_eq!(branch1.files.len(), 1); assert_eq!(branch1.files.len(), 1);
assert_eq!(branch1.commits.len(), 1); assert_eq!(branch1.commits.len(), 1);
@ -1054,7 +1046,7 @@ fn test_merge_vbranch_upstream_clean_rebase() -> Result<()> {
None, None,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches[0]; let branch1 = &branches[0];
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?; let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;
@ -1163,7 +1155,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
.context("failed to write target branch after push")?; .context("failed to write target branch after push")?;
// create the branch // create the branch
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches[0]; let branch1 = &branches[0];
assert_eq!(branch1.files.len(), 1); assert_eq!(branch1.files.len(), 1);
@ -1172,7 +1164,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
merge_virtual_branch_upstream(&gb_repository, &project_repository, &branch1.id, None, None)?; merge_virtual_branch_upstream(&gb_repository, &project_repository, &branch1.id, None, None)?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches[0]; let branch1 = &branches[0];
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?; let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;
@ -1192,7 +1184,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
)?; )?;
// make gb see the conflict resolution // make gb see the conflict resolution
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
assert!(branches[0].conflicted); assert!(branches[0].conflicted);
// commit the merge resolution // commit the merge resolution
@ -1207,7 +1199,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches[0]; let branch1 = &branches[0];
assert!(!branch1.conflicted); assert!(!branch1.conflicted);
assert_eq!(branch1.files.len(), 0); assert_eq!(branch1.files.len(), 0);
@ -1247,7 +1239,7 @@ fn test_unapply_ownership_partial() -> Result<()> {
) )
.expect("failed to create virtual branch"); .expect("failed to create virtual branch");
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
assert_eq!(branches.len(), 1); assert_eq!(branches.len(), 1);
assert_eq!(branches[0].files.len(), 1); assert_eq!(branches[0].files.len(), 1);
assert_eq!(branches[0].ownership.files.len(), 1); assert_eq!(branches[0].ownership.files.len(), 1);
@ -1261,11 +1253,11 @@ fn test_unapply_ownership_partial() -> Result<()> {
unapply_ownership( unapply_ownership(
&gb_repository, &gb_repository,
&project_repository, &project_repository,
&"test.txt:5-6".parse().unwrap(), &"test.txt:2-6".parse().unwrap(),
) )
.unwrap(); .unwrap();
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
assert_eq!(branches.len(), 1); assert_eq!(branches.len(), 1);
assert_eq!(branches[0].files.len(), 0); assert_eq!(branches[0].files.len(), 0);
assert_eq!(branches[0].ownership.files.len(), 0); assert_eq!(branches[0].ownership.files.len(), 0);
@ -1339,7 +1331,7 @@ fn test_apply_unapply_branch() -> Result<()> {
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?; let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
assert_eq!("line5\nline6\n", String::from_utf8(contents)?); assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
assert!(branch.active); assert!(branch.active);
@ -1351,7 +1343,7 @@ fn test_apply_unapply_branch() -> Result<()> {
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?; let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
assert_eq!("line5\nline6\n", String::from_utf8(contents)?); assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
assert!(!branch.active); assert!(!branch.active);
@ -1365,7 +1357,7 @@ fn test_apply_unapply_branch() -> Result<()> {
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?; let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
assert_eq!("line5\nline6\n", String::from_utf8(contents)?); assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
assert!(branch.active); assert!(branch.active);
@ -1621,7 +1613,7 @@ fn test_detect_mergeable_branch() -> Result<()> {
}; };
branch_writer.write(&mut branch4)?; branch_writer.write(&mut branch4)?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
assert_eq!(branches.len(), 4); assert_eq!(branches.len(), 4);
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
@ -1803,7 +1795,7 @@ fn test_upstream_integrated_vbranch() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert!(branch1.commits.iter().any(|c| c.is_integrated)); assert!(branch1.commits.iter().any(|c| c.is_integrated));
@ -1850,7 +1842,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n", "line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
@ -1869,7 +1861,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 0, "no files expected"); assert_eq!(branch.files.len(), 0, "no files expected");
@ -1889,7 +1881,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
"line1\nPATCH1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n", "line1\nPATCH1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1, "one file should be changed"); assert_eq!(branch.files.len(), 1, "one file should be changed");
@ -1906,7 +1898,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!( assert_eq!(
@ -1951,7 +1943,7 @@ fn test_commit_same_file_twice() -> Result<()> {
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n", "line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
@ -1970,7 +1962,7 @@ fn test_commit_same_file_twice() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 0, "no files expected"); assert_eq!(branch.files.len(), 0, "no files expected");
@ -1990,7 +1982,7 @@ fn test_commit_same_file_twice() -> Result<()> {
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n", "line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1, "one file should be changed"); assert_eq!(branch.files.len(), 1, "one file should be changed");
@ -2007,7 +1999,7 @@ fn test_commit_same_file_twice() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!( assert_eq!(
@ -2052,7 +2044,7 @@ fn test_commit_partial_by_hunk() -> Result<()> {
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n", "line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
@ -2065,13 +2057,13 @@ fn test_commit_partial_by_hunk() -> Result<()> {
&project_repository, &project_repository,
&branch1_id, &branch1_id,
"first commit to test.txt", "first commit to test.txt",
Some(&"test.txt:2-3".parse::<Ownership>().unwrap()), Some(&"test.txt:1-6".parse::<Ownership>().unwrap()),
None, None,
None, None,
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 1); assert_eq!(branch.files.len(), 1);
@ -2085,13 +2077,13 @@ fn test_commit_partial_by_hunk() -> Result<()> {
&project_repository, &project_repository,
&branch1_id, &branch1_id,
"second commit to test.txt", "second commit to test.txt",
Some(&"test.txt:19-20".parse::<Ownership>().unwrap()), Some(&"test.txt:16-22".parse::<Ownership>().unwrap()),
None, None,
None, None,
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
assert_eq!(branch.files.len(), 0); assert_eq!(branch.files.len(), 0);
@ -2158,7 +2150,7 @@ fn test_commit_partial_by_file() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
// branch one test.txt has just the 1st and 3rd hunks applied // branch one test.txt has just the 1st and 3rd hunks applied
@ -2234,7 +2226,7 @@ fn test_commit_add_and_delete_files() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
// branch one test.txt has just the 1st and 3rd hunks applied // branch one test.txt has just the 1st and 3rd hunks applied
@ -2305,7 +2297,7 @@ fn test_commit_executable_and_symlinks() -> Result<()> {
false, false,
)?; )?;
let branches = list_virtual_branches(&gb_repository, &project_repository)?; let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap(); let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
let commit = &branch1.commits[0].id; let commit = &branch1.commits[0].id;
@ -2407,7 +2399,7 @@ fn test_verify_branch_commits_to_integration() -> Result<()> {
integration::verify_branch(&gb_repository, &project_repository).unwrap(); integration::verify_branch(&gb_repository, &project_repository).unwrap();
// one virtual branch with two commits was created // one virtual branch with two commits was created
let virtual_branches = list_virtual_branches(&gb_repository, &project_repository)?; let (virtual_branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
assert_eq!(virtual_branches.len(), 1); assert_eq!(virtual_branches.len(), 1);
let branch = &virtual_branches.first().unwrap(); let branch = &virtual_branches.first().unwrap();

View File

@ -4,8 +4,9 @@ use std::{collections::HashMap, path, time, vec};
use std::os::unix::prelude::*; use std::os::unix::prelude::*;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use diffy::{apply_bytes, Patch}; use bstr::ByteSlice;
use git2_hooks::{HookResult, PrepareCommitMsgSource}; use diffy::{apply, Patch};
use git2_hooks::HookResult;
use regex::Regex; use regex::Regex;
use serde::Serialize; use serde::Serialize;
@ -728,7 +729,7 @@ fn find_base_tree<'a>(
pub fn list_virtual_branches( pub fn list_virtual_branches(
gb_repository: &gb_repository::Repository, gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository, project_repository: &project_repository::Repository,
) -> Result<Vec<VirtualBranch>, errors::ListVirtualBranchesError> { ) -> Result<(Vec<VirtualBranch>, bool), errors::ListVirtualBranchesError> {
let mut branches: Vec<VirtualBranch> = Vec::new(); let mut branches: Vec<VirtualBranch> = Vec::new();
let default_target = gb_repository let default_target = gb_repository
@ -889,6 +890,9 @@ pub fn list_virtual_branches(
let branches = branches_with_large_files_abridged(branches); let branches = branches_with_large_files_abridged(branches);
let mut branches = branches_with_hunk_locks(branches, project_repository)?; let mut branches = branches_with_hunk_locks(branches, project_repository)?;
// If there no context lines are used internally, add them here, before returning to the UI
if context_lines(project_repository) == 0 {
for branch in &mut branches { for branch in &mut branches {
branch.files = files_with_hunk_context( branch.files = files_with_hunk_context(
&project_repository.git_repository, &project_repository.git_repository,
@ -898,12 +902,17 @@ pub fn list_virtual_branches(
) )
.context("failed to add hunk context")?; .context("failed to add hunk context")?;
} }
}
branches.sort_by(|a, b| a.order.cmp(&b.order)); branches.sort_by(|a, b| a.order.cmp(&b.order));
super::integration::update_gitbutler_integration(gb_repository, project_repository)?; super::integration::update_gitbutler_integration(gb_repository, project_repository)?;
Ok(branches) let uses_diff_context = project_repository
.project()
.use_diff_context
.unwrap_or(false);
Ok((branches, uses_diff_context))
} }
fn branches_with_large_files_abridged(mut branches: Vec<VirtualBranch>) -> Vec<VirtualBranch> { fn branches_with_large_files_abridged(mut branches: Vec<VirtualBranch>) -> Vec<VirtualBranch> {
@ -940,6 +949,7 @@ fn branches_with_hunk_locks(
&project_repository.git_repository, &project_repository.git_repository,
&parent_tree, &parent_tree,
&commit_tree, &commit_tree,
context_lines(project_repository),
)?; )?;
for branch in &mut branches { for branch in &mut branches {
for file in &mut branch.files { for file in &mut branch.files {
@ -1091,6 +1101,7 @@ pub fn calculate_non_commited_diffs(
&project_repository.git_repository, &project_repository.git_repository,
&branch_head, &branch_head,
&branch_tree, &branch_tree,
context_lines(project_repository),
) )
.context("failed to diff trees")?; .context("failed to diff trees")?;
@ -1132,6 +1143,7 @@ fn list_virtual_commit_files(
&project_repository.git_repository, &project_repository.git_repository,
&parent_tree, &parent_tree,
&commit_tree, &commit_tree,
context_lines(project_repository),
)?; )?;
let hunks_by_filepath = virtual_hunks_by_filepath(&project_repository.project().path, &diff); let hunks_by_filepath = virtual_hunks_by_filepath(&project_repository.project().path, &diff);
Ok(virtual_hunks_to_virtual_files( Ok(virtual_hunks_to_virtual_files(
@ -1895,6 +1907,7 @@ fn get_non_applied_status(
&project_repository.git_repository, &project_repository.git_repository,
&target_tree, &target_tree,
&branch_tree, &branch_tree,
context_lines(project_repository),
)?; )?;
Ok((branch, diff)) Ok((branch, diff))
@ -1913,7 +1926,11 @@ fn get_applied_status(
default_target: &target::Target, default_target: &target::Target,
mut virtual_branches: Vec<branch::Branch>, mut virtual_branches: Vec<branch::Branch>,
) -> Result<AppliedStatuses> { ) -> Result<AppliedStatuses> {
let mut diff = diff::workdir(&project_repository.git_repository, &default_target.sha) let mut diff = diff::workdir(
&project_repository.git_repository,
&default_target.sha,
context_lines(project_repository),
)
.context("failed to diff workdir")?; .context("failed to diff workdir")?;
// sort by order, so that the default branch is first (left in the ui) // sort by order, so that the default branch is first (left in the ui)
@ -2291,39 +2308,37 @@ pub fn write_tree_onto_tree(
.peel_to_blob() .peel_to_blob()
.context("failed to get blob")?; .context("failed to get blob")?;
// get the contents let mut blob_contents = blob.content().to_str()?.to_string();
let mut blob_contents = blob.content().to_vec();
let mut hunks = hunks.clone(); let mut hunks = hunks.clone();
hunks.sort_by_key(|hunk| hunk.new_start); hunks.sort_by_key(|hunk| hunk.new_start);
let mut all_diffs = String::new();
for hunk in hunks { for hunk in hunks {
let patch = format!("--- original\n+++ modified\n{}", hunk.diff); all_diffs.push_str(&hunk.diff);
let patch_bytes = patch.as_bytes();
let patch = Patch::from_bytes(patch_bytes)?;
blob_contents = apply_bytes(&blob_contents, &patch)
.context(format!("failed to apply {}", &hunk.diff))?;
} }
let patch = Patch::from_str(&all_diffs)?;
blob_contents = apply(&blob_contents, &patch)
.context(format!("failed to apply {}", &all_diffs))?;
// create a blob // create a blob
let new_blob_oid = git_repository.blob(&blob_contents)?; let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?;
// upsert into the builder // upsert into the builder
builder.upsert(rel_path, new_blob_oid, filemode); builder.upsert(rel_path, new_blob_oid, filemode);
} }
} else if is_submodule { } else if is_submodule {
let mut blob_contents = vec![]; let mut blob_contents = String::new();
let mut hunks = hunks.clone(); let mut hunks = hunks.clone();
hunks.sort_by_key(|hunk| hunk.new_start); hunks.sort_by_key(|hunk| hunk.new_start);
for hunk in hunks { for hunk in hunks {
let patch = format!("--- original\n+++ modified\n{}", hunk.diff); let patch = Patch::from_str(&hunk.diff)?;
let patch_bytes = patch.as_bytes(); blob_contents = apply(&blob_contents, &patch)
let patch = Patch::from_bytes(patch_bytes)?;
blob_contents = apply_bytes(&blob_contents, &patch)
.context(format!("failed to apply {}", &hunk.diff))?; .context(format!("failed to apply {}", &hunk.diff))?;
} }
// create a blob // create a blob
let new_blob_oid = git_repository.blob(&blob_contents)?; let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?;
// upsert into the builder // upsert into the builder
builder.upsert(rel_path, new_blob_oid, filemode); builder.upsert(rel_path, new_blob_oid, filemode);
} else { } else {
@ -3636,6 +3651,7 @@ pub fn move_commit(
&project_repository.git_repository, &project_repository.git_repository,
&source_branch_head_parent_tree, &source_branch_head_parent_tree,
&source_branch_head_tree, &source_branch_head_tree,
context_lines(project_repository),
)?; )?;
let is_source_locked = source_branch_non_comitted_files let is_source_locked = source_branch_non_comitted_files
@ -3836,6 +3852,7 @@ pub fn create_virtual_branch_from_branch(
&project_repository.git_repository, &project_repository.git_repository,
&merge_base_tree, &merge_base_tree,
&head_commit_tree, &head_commit_tree,
context_lines(project_repository),
) )
.context("failed to diff trees")?; .context("failed to diff trees")?;
@ -3899,6 +3916,19 @@ pub fn create_virtual_branch_from_branch(
} }
} }
pub fn context_lines(project_repository: &project_repository::Repository) -> u32 {
let use_context = project_repository
.project()
.use_diff_context
.unwrap_or(false);
if use_context {
3_u32
} else {
0_u32
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -79,7 +79,7 @@ impl InnerHandler {
.list_virtual_branches(project_id) .list_virtual_branches(project_id)
.await .await
{ {
Ok(branches) => Ok(vec![events::Event::Emit( Ok((branches, _)) => Ok(vec![events::Event::Emit(
app_events::Event::virtual_branches( app_events::Event::virtual_branches(
project_id, project_id,
&self.assets_proxy.proxy_virtual_branches(branches).await, &self.assets_proxy.proxy_virtual_branches(branches).await,

View File

@ -27,7 +27,7 @@
}, },
"updater": { "updater": {
"active": true, "active": true,
"dialog": true, "dialog": false,
"endpoints": [ "endpoints": [
"https://app.gitbutler.com/releases/nightly/{{target}}-{{arch}}/{{current_version}}" "https://app.gitbutler.com/releases/nightly/{{target}}-{{arch}}/{{current_version}}"
], ],

View File

@ -27,7 +27,7 @@
}, },
"updater": { "updater": {
"active": true, "active": true,
"dialog": true, "dialog": false,
"endpoints": [ "endpoints": [
"https://app.gitbutler.com/releases/release/{{target}}-{{arch}}/{{current_version}}" "https://app.gitbutler.com/releases/release/{{target}}-{{arch}}/{{current_version}}"
], ],

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ export type Project = {
preferred_key: Key; preferred_key: Key;
ok_with_force_push: boolean; ok_with_force_push: boolean;
omit_certificate_check: boolean | undefined; omit_certificate_check: boolean | undefined;
use_diff_context: boolean | undefined;
}; };
export class ProjectService { export class ProjectService {

View File

@ -0,0 +1,122 @@
import { showToast } from '$lib/notifications/toasts';
import {
checkUpdate,
installUpdate,
onUpdaterEvent,
type UpdateResult,
type UpdateStatus
} from '@tauri-apps/api/updater';
import posthog from 'posthog-js';
import {
BehaviorSubject,
switchMap,
Observable,
from,
map,
shareReplay,
interval,
timeout,
catchError,
of,
startWith,
combineLatestWith,
tap
} from 'rxjs';
// TOOD: Investigate why 'DOWNLOADED' is not in the type provided by Tauri.
export type Update =
| { version?: string; status?: UpdateStatus | 'DOWNLOADED'; body?: string }
| undefined;
export class UpdaterService {
private reload$ = new BehaviorSubject<void>(undefined);
private status$ = new BehaviorSubject<UpdateStatus | undefined>(undefined);
/**
* Example output:
* {version: "0.5.303", date: "2024-02-25 3:09:58.0 +00:00:00", body: "", status: "DOWNLOADED"}
*/
update$: Observable<Update>;
// We don't ever call this because the class is meant to be used as a singleton
unlistenFn: any;
constructor() {
onUpdaterEvent((status) => {
const err = status.error;
if (err) showErrorToast(err);
this.status$.next(status.status);
}).then((unlistenFn) => (this.unlistenFn = unlistenFn));
this.update$ = this.reload$.pipe(
// Now and then every hour indefinitely
switchMap(() => interval(60 * 60 * 1000).pipe(startWith(0))),
tap(() => this.status$.next(undefined)),
// Timeout needed to prevent hanging in dev mode
switchMap(() => from(checkUpdate()).pipe(timeout(10000))),
// The property `shouldUpdate` seems useless, only indicates presence of manifest
map((update: UpdateResult | undefined) => {
if (update?.shouldUpdate) return update.manifest;
}),
// Hide offline/timeout errors since no app ever notifies you about this
catchError((err) => {
if (!isOffline(err) && !isTimeoutError(err)) {
posthog.capture('Updater Check Error', err);
showErrorToast(err);
console.log(err);
}
return of(undefined);
}),
// Status is irrelevant without a proposed update so we merge the streams
combineLatestWith(this.status$),
map(([update, status]) => {
if (update) return { ...update, status };
return undefined;
}),
shareReplay(1)
);
// Use this for testing component manually (until we have actual tests)
// this.update$ = new BehaviorSubject({
// version: '0.5.303',
// date: '2024-02-25 3:09:58.0 +00:00:00',
// body: '- Improves the performance of virtual branch operations (quicker and lower CPU usage)\n- Large numbers of hunks for a file will only be rendered in the UI after confirmation'
// });
}
async install() {
try {
await installUpdate();
posthog.capture('App Update Successful');
} catch (e: any) {
// We expect toast to be shown by error handling in `onUpdaterEvent`
posthog.capture('App Update Failed', e);
}
}
}
function isOffline(err: any): boolean {
return typeof err == 'string' && err.includes('Could not fetch a valid release');
}
function isTimeoutError(err: any): boolean {
return err?.name == 'TimeoutError';
}
function showErrorToast(err: any) {
if (isOffline(err)) return;
showToast({
title: 'App update failed',
message: `
Something went wrong while updating the app.
You can download the latest release from our
[downloads](https://app.gitbutler.com/downloads) page.
\`\`\`
${err}
\`\`\`
`,
style: 'error'
});
posthog.capture('Updater Status Error', err);
}

View File

@ -5,6 +5,7 @@ import { startWith, switchMap } from 'rxjs/operators';
import type { GitHubService } from '$lib/github/service'; import type { GitHubService } from '$lib/github/service';
import type { PullRequest } from '$lib/github/types'; import type { PullRequest } from '$lib/github/types';
import type { RemoteBranchService } from '$lib/stores/remoteBranches'; import type { RemoteBranchService } from '$lib/stores/remoteBranches';
import type { BranchController } from '$lib/vbranches/branchController';
import type { VirtualBranchService } from '$lib/vbranches/branchStoresCache'; import type { VirtualBranchService } from '$lib/vbranches/branchStoresCache';
import type { Branch, RemoteBranch } from '$lib/vbranches/types'; import type { Branch, RemoteBranch } from '$lib/vbranches/types';
@ -14,7 +15,8 @@ export class BranchService {
constructor( constructor(
private vbranchService: VirtualBranchService, private vbranchService: VirtualBranchService,
remoteBranchService: RemoteBranchService, remoteBranchService: RemoteBranchService,
private githubService: GitHubService private githubService: GitHubService,
private branchController: BranchController
) { ) {
const vbranchesWithEmpty$ = vbranchService.branches$.pipe(startWith([])); const vbranchesWithEmpty$ = vbranchService.branches$.pipe(startWith([]));
const branchesWithEmpty$ = remoteBranchService.branches$.pipe(startWith([])); const branchesWithEmpty$ = remoteBranchService.branches$.pipe(startWith([]));
@ -47,7 +49,7 @@ export class BranchService {
// Push if local commits // Push if local commits
if (branch.commits.some((c) => !c.isRemote)) { if (branch.commits.some((c) => !c.isRemote)) {
newBranch = await this.vbranchService.pushBranch(branch.id, branch.requiresForce); newBranch = await this.branchController.pushBranch(branch.id, branch.requiresForce);
} else { } else {
newBranch = branch; newBranch = branch;
} }

View File

@ -49,6 +49,8 @@
color var(--transition-fast), color var(--transition-fast),
filter var(--transition-fast); filter var(--transition-fast);
cursor: pointer;
&.pop { &.pop {
color: var(--clr-theme-scale-pop-10); color: var(--clr-theme-scale-pop-10);
background: color-mix( background: color-mix(

View File

@ -1,4 +1,5 @@
<script lang="ts" async="true"> <script lang="ts" async="true">
import FullscreenLoading from './FullscreenLoading.svelte';
import NewBranchDropZone from './NewBranchDropZone.svelte'; import NewBranchDropZone from './NewBranchDropZone.svelte';
import BranchLane from '$lib/components/BranchLane.svelte'; import BranchLane from '$lib/components/BranchLane.svelte';
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/Icon.svelte';
@ -39,7 +40,7 @@
{#if branchesError} {#if branchesError}
<div class="p-4" data-tauri-drag-region>Something went wrong...</div> <div class="p-4" data-tauri-drag-region>Something went wrong...</div>
{:else if !branches} {:else if !branches}
<div class="loading" data-tauri-drag-region><Icon name="spinner" /></div> <FullscreenLoading />
{:else} {:else}
<div <div
class="board" class="board"
@ -213,13 +214,6 @@
height: 100%; height: 100%;
} }
.loading {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
/* Empty board */ /* Empty board */
.empty-board { .empty-board {

View File

@ -69,15 +69,15 @@
} }
} }
.branch-files__header { .branch-files__header {
padding-top: var(--space-12); padding-top: var(--space-14);
padding-bottom: var(--space-12); padding-bottom: var(--space-12);
padding-left: var(--space-20); padding-left: var(--space-14);
padding-right: var(--space-12); padding-right: var(--space-14);
} }
.files-padding { .files-padding {
padding-top: 0; padding-top: 0;
padding-bottom: var(--space-12); padding-bottom: var(--space-12);
padding-left: var(--space-12); padding-left: var(--space-14);
padding-right: var(--space-12); padding-right: var(--space-14);
} }
</style> </style>

View File

@ -407,7 +407,7 @@
display: flex; display: flex;
gap: var(--space-4); gap: var(--space-4);
background: var(--clr-theme-container-pale); background: var(--clr-theme-container-pale);
padding: var(--space-12); padding: var(--space-14);
justify-content: space-between; justify-content: space-between;
border-radius: 0 0 var(--radius-m) var(--radius-m); border-radius: 0 0 var(--radius-m) var(--radius-m);
user-select: none; user-select: none;
@ -452,7 +452,8 @@
.header__remote-branch { .header__remote-branch {
color: var(--clr-theme-scale-ntrl-50); color: var(--clr-theme-scale-ntrl-50);
padding-left: var(--space-4); padding-left: var(--space-2);
padding-right: var(--space-2);
display: flex; display: flex;
gap: var(--space-4); gap: var(--space-4);
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -139,7 +139,6 @@
width: 100%; width: 100%;
height: var(--space-20); height: var(--space-20);
background: var(--target-branch-background); background: var(--target-branch-background);
/* background-color: red; */
} }
.header__info { .header__info {
display: flex; display: flex;
@ -152,7 +151,7 @@
display: flex; display: flex;
gap: var(--space-4); gap: var(--space-4);
background: var(--clr-theme-container-pale); background: var(--clr-theme-container-pale);
padding: var(--space-12); padding: var(--space-14);
justify-content: space-between; justify-content: space-between;
border-radius: 0 0 var(--radius-m) var(--radius-m); border-radius: 0 0 var(--radius-m) var(--radius-m);
user-select: none; user-select: none;
@ -171,7 +170,8 @@
.header__remote-branch { .header__remote-branch {
color: var(--clr-theme-scale-ntrl-50); color: var(--clr-theme-scale-ntrl-50);
padding-left: var(--space-4); padding-left: var(--space-2);
padding-right: var(--space-2);
display: flex; display: flex;
gap: var(--space-4); gap: var(--space-4);
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -4,10 +4,12 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/Icon.svelte'; import Icon from '$lib/components/Icon.svelte';
import { pxToRem } from '$lib/utils/pxToRem';
import { tooltip } from '$lib/utils/tooltip'; import { tooltip } from '$lib/utils/tooltip';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type iconsJson from '$lib/icons/icons.json'; import type iconsJson from '$lib/icons/icons.json';
export let size: 'medium' | 'large' = 'medium';
export let icon: keyof typeof iconsJson | undefined = undefined; export let icon: keyof typeof iconsJson | undefined = undefined;
export let iconAlign: 'left' | 'right' = 'right'; export let iconAlign: 'left' | 'right' = 'right';
export let color: ButtonColor = 'primary'; export let color: ButtonColor = 'primary';
@ -21,6 +23,7 @@
export let grow = false; export let grow = false;
export let align: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'auto' = 'auto'; export let align: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'auto' = 'auto';
export let help = ''; export let help = '';
export let width: number | undefined = undefined;
export let element: HTMLAnchorElement | HTMLButtonElement | HTMLElement | null = null; export let element: HTMLAnchorElement | HTMLButtonElement | HTMLElement | null = null;
@ -37,6 +40,8 @@
<button <button
class={`btn ${className}`} class={`btn ${className}`}
class:medium={size == 'medium'}
class:large={size == 'large'}
class:error-outline={color == 'error' && kind == 'outlined'} class:error-outline={color == 'error' && kind == 'outlined'}
class:primary-outline={color == 'primary' && kind == 'outlined'} class:primary-outline={color == 'primary' && kind == 'outlined'}
class:warn-outline={color == 'warn' && kind == 'outlined'} class:warn-outline={color == 'warn' && kind == 'outlined'}
@ -51,6 +56,7 @@
class:grow class:grow
class:not-clickable={notClickable} class:not-clickable={notClickable}
style:align-self={align} style:align-self={align}
style:width={width ? pxToRem(width) : undefined}
use:tooltip={help} use:tooltip={help}
bind:this={element} bind:this={element}
disabled={disabled || loading} disabled={disabled || loading}
@ -84,7 +90,9 @@
min-width: var(--size-btn-m); min-width: var(--size-btn-m);
background: transparent; background: transparent;
transition: background-color var(--transition-fast); transition: background-color var(--transition-fast);
cursor: pointer;
&:disabled { &:disabled {
cursor: default;
pointer-events: none; pointer-events: none;
opacity: 0.6; opacity: 0.6;
} }
@ -99,6 +107,7 @@
flex-direction: row-reverse; flex-direction: row-reverse;
} }
&.not-clickable { &.not-clickable {
cursor: default;
pointer-events: none; pointer-events: none;
} }
} }
@ -177,4 +186,16 @@
border: 1px solid color-mix(in srgb, var(--clr-theme-err-outline), var(--darken-mid)); border: 1px solid color-mix(in srgb, var(--clr-theme-err-outline), var(--darken-mid));
} }
} }
/* SIZE MODIFIERS */
.btn.medium {
height: var(--size-btn-m);
padding: var(--space-4) var(--space-6);
}
.btn.large {
height: var(--size-btn-l);
padding: var(--space-6) var(--space-8);
}
</style> </style>

View File

@ -26,6 +26,7 @@
class="checkbox" class="checkbox"
class:small class:small
{value} {value}
id={name}
{name} {name}
{disabled} {disabled}
/> />
@ -33,6 +34,7 @@
<style lang="postcss"> <style lang="postcss">
.checkbox { .checkbox {
appearance: none; appearance: none;
cursor: pointer;
width: var(--space-16); width: var(--space-16);
height: var(--space-16); height: var(--space-16);
flex-shrink: 0; flex-shrink: 0;

View File

@ -3,19 +3,34 @@
export let padding: string = 'var(--space-16)'; export let padding: string = 'var(--space-16)';
export let disabled = false; export let disabled = false;
export let checked = false;
export let hasTopRadius = true;
export let hasBottomRadius = true;
export let hasBottomLine = true;
const SLOTS = $$props.$$slots; const SLOTS = $$props.$$slots;
const dispatch = createEventDispatcher<{ const dispatchClick = createEventDispatcher<{
click: void; click: void;
}>(); }>();
const dispatchChange = createEventDispatcher<{
change: boolean;
}>();
</script> </script>
<button <button
class="clickable-card" class="clickable-card"
class:has-top-radius={hasTopRadius}
class:has-bottom-radius={hasBottomRadius}
class:has-bottom-line={hasBottomLine}
style="padding: {padding}" style="padding: {padding}"
on:click={() => { on:click={() => {
dispatch('click'); dispatchClick('click');
dispatchChange('change', checked);
checked = !checked;
}} }}
class:card-disabled={disabled} class:card-disabled={disabled}
{disabled} {disabled}
@ -43,8 +58,8 @@
.clickable-card { .clickable-card {
display: flex; display: flex;
gap: var(--space-16); gap: var(--space-16);
border-radius: var(--radius-l); border-left: 1px solid var(--clr-theme-container-outline-light);
border: 1px solid var(--clr-theme-container-outline-light); border-right: 1px solid var(--clr-theme-container-outline-light);
background-color: var(--clr-theme-container-light); background-color: var(--clr-theme-container-light);
transition: transition:
background-color var(--transition-fast), background-color var(--transition-fast),
@ -84,4 +99,21 @@
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
} }
/* MODIFIERS */
.has-top-radius {
border-top: 1px solid var(--clr-theme-container-outline-light);
border-top-left-radius: var(--radius-l);
border-top-right-radius: var(--radius-l);
}
.has-bottom-radius {
border-bottom-left-radius: var(--radius-l);
border-bottom-right-radius: var(--radius-l);
}
.has-bottom-line {
border-bottom: 1px solid var(--clr-theme-container-outline-light);
}
</style> </style>

View File

@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import { getCloudApiClient, type User } from '$lib/backend/cloud'; import { getCloudApiClient, type User } from '$lib/backend/cloud';
import Checkbox from '$lib/components/Checkbox.svelte'; import ClickableCard from '$lib/components/ClickableCard.svelte';
import Link from '$lib/components/Link.svelte'; import Link from '$lib/components/Link.svelte';
import Login from '$lib/components/Login.svelte'; import Spacer from '$lib/components/Spacer.svelte';
import Toggle from '$lib/components/Toggle.svelte';
import WelcomeSigninAction from '$lib/components/WelcomeSigninAction.svelte';
import { projectAiGenEnabled } from '$lib/config/config'; import { projectAiGenEnabled } from '$lib/config/config';
import { projectAiGenAutoBranchNamingEnabled } from '$lib/config/config'; import { projectAiGenAutoBranchNamingEnabled } from '$lib/config/config';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
@ -47,76 +49,58 @@
toasts.error('Failed to update project sync status'); toasts.error('Failed to update project sync status');
} }
}; };
</script>
<section class="space-y-2"> const aiGenToggle = () => {
{#if user}
<h2 class="text-xl">GitButler Cloud</h2>
<header>
<span class="text-text-subdued"> Summary generation </span>
</header>
<div
class="flex flex-row items-center justify-between rounded-lg border border-light-400 p-2 dark:border-dark-500"
>
<div class="flex flex-col space-x-3">
<div class="flex flex-row items-center gap-x-1">
<Checkbox
name="sync"
disabled={user === undefined}
checked={$aiGenEnabled}
on:change={() => {
$aiGenEnabled = !$aiGenEnabled; $aiGenEnabled = !$aiGenEnabled;
$aiGenAutoBranchNamingEnabled = $aiGenEnabled; $aiGenAutoBranchNamingEnabled = $aiGenEnabled;
}} };
/>
<label class="ml-2" for="sync">Enable branch and commit message generation.</label> const aiGenBranchNamesToggle = () => {
</div> $aiGenAutoBranchNamingEnabled = !$aiGenAutoBranchNamingEnabled;
<div class="pl-4 pr-8 text-sm text-light-700 dark:text-dark-200"> };
</script>
{#if user}
<div class="aigen-wrap">
<ClickableCard on:click={aiGenToggle}>
<svelte:fragment slot="title">Enable branch and commit message generation</svelte:fragment>
<svelte:fragment slot="body">
Uses OpenAI's API. If enabled, diffs will sent to OpenAI's servers when pressing the Uses OpenAI's API. If enabled, diffs will sent to OpenAI's servers when pressing the
"Generate message" button. "Generate message" button.
</div> </svelte:fragment>
<div class="flex flex-col space-x-3"> <svelte:fragment slot="actions">
<div class="flex flex-row items-center gap-x-1"> <Toggle checked={$aiGenEnabled} on:change={aiGenToggle} />
<Checkbox </svelte:fragment>
name="sync" </ClickableCard>
disabled={user === undefined || !$aiGenEnabled}
<ClickableCard disabled={!$aiGenEnabled} on:click={aiGenBranchNamesToggle}>
<svelte:fragment slot="title">Automatically generate branch names</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle
disabled={!$aiGenEnabled}
checked={$aiGenAutoBranchNamingEnabled} checked={$aiGenAutoBranchNamingEnabled}
on:change={() => { on:change={aiGenBranchNamesToggle}
$aiGenAutoBranchNamingEnabled = !$aiGenAutoBranchNamingEnabled;
}}
/> />
<label class="ml-2" for="sync">Automatically generate branch names.</label> </svelte:fragment>
</div> </ClickableCard>
</div>
</div>
</div>
{#if user.role === 'admin'}
<header>
<span class="text-text-subdued"> Full data synchronization </span>
</header>
<div
class="flex flex-row items-center justify-between rounded-lg border border-light-400 p-2 dark:border-dark-500"
>
<div class="flex flex-row space-x-3">
<div class="flex flex-row items-center gap-1">
<Checkbox
name="sync"
disabled={user === undefined}
checked={project.api?.sync || false}
on:change={onSyncChange}
/>
<label class="ml-2" for="sync">
Sync my history, repository and branch data for backup, sharing and team features.
</label>
</div>
</div>
</div> </div>
<Spacer />
{#if user.role === 'admin'}
<h3 class="text-base-15 text-bold">Full data synchronization</h3>
<ClickableCard on:change={onSyncChange}>
<svelte:fragment slot="body">
Sync my history, repository and branch data for backup, sharing and team features.
</svelte:fragment>
<svelte:fragment slot="actions">
<Toggle checked={project.api?.sync || false} on:change={onSyncChange} />
</svelte:fragment>
</ClickableCard>
{#if project.api} {#if project.api}
<div class="flex flex-row justify-end space-x-2"> <div class="api-link">
<div class="p-1">
<Link <Link
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
@ -124,10 +108,23 @@
>Go to GitButler Cloud Project</Link >Go to GitButler Cloud Project</Link
> >
</div> </div>
</div>
{/if} {/if}
<Spacer />
{/if} {/if}
{:else} {:else}
<Login {userService} /> <WelcomeSigninAction {userService} />
{/if} <Spacer />
</section> {/if}
<style lang="post-css">
.aigen-wrap {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.api-link {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -154,7 +154,7 @@
.commit { .commit {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
cursor: default;
border-radius: var(--space-6); border-radius: var(--space-6);
background-color: var(--clr-theme-container-light); background-color: var(--clr-theme-container-light);
border: 1px solid var(--clr-theme-container-outline-light); border: 1px solid var(--clr-theme-container-outline-light);
@ -173,10 +173,11 @@
} }
.commit__header { .commit__header {
cursor: pointer;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-10); gap: var(--space-10);
padding: var(--space-12); padding: var(--space-14);
} }
.is-commit-open { .is-commit-open {
@ -263,7 +264,7 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: var(--space-8); gap: var(--space-8);
padding: var(--space-12); padding: var(--space-14);
border-top: 1px solid var(--clr-theme-container-outline-light); border-top: 1px solid var(--clr-theme-container-outline-light);
} }
</style> </style>

View File

@ -135,7 +135,7 @@
on:focus={useAutoHeight} on:focus={useAutoHeight}
on:change={() => currentCommitMessage.set(commitMessage)} on:change={() => currentCommitMessage.set(commitMessage)}
spellcheck={false} spellcheck={false}
class="text-input commit-box__textarea" class="text-input text-base-body-13 commit-box__textarea"
rows="1" rows="1"
disabled={isGeneratingCommigMessage} disabled={isGeneratingCommigMessage}
placeholder="Your commit message here" placeholder="Your commit message here"
@ -217,7 +217,7 @@
.commit-box { .commit-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: var(--space-16); padding: var(--space-14);
background: var(--clr-theme-container-light); background: var(--clr-theme-container-light);
border-top: 1px solid var(--clr-theme-container-outline-light); border-top: 1px solid var(--clr-theme-container-outline-light);
transition: background-color var(--transition-medium); transition: background-color var(--transition-medium);

View File

@ -91,7 +91,7 @@
.commit-list__content { .commit-list__content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0 var(--space-16) var(--space-20) var(--space-16); padding: 0 var(--space-14) var(--space-20) var(--space-14);
gap: var(--space-8); gap: var(--space-8);
} }
.upstream-message { .upstream-message {

View File

@ -35,7 +35,7 @@
.header { .header {
display: flex; display: flex;
align-items: center; align-items: center;
padding: var(--space-16) var(--space-12) var(--space-16) var(--space-16); padding: var(--space-16) var(--space-14) var(--space-16) var(--space-14);
justify-content: space-between; justify-content: space-between;
gap: var(--space-8); gap: var(--space-8);

View File

@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import SectionCard from '$lib/components/SectionCard.svelte';
import Spacer from '$lib/components/Spacer.svelte';
import TextArea from '$lib/components/TextArea.svelte'; import TextArea from '$lib/components/TextArea.svelte';
import TextBox from '$lib/components/TextBox.svelte'; import TextBox from '$lib/components/TextBox.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@ -14,15 +16,13 @@
}>(); }>();
</script> </script>
<form class="flex flex-col gap-3"> <SectionCard>
<fieldset class="flex flex-col gap-3"> <form>
<div class="flex flex-col gap-1"> <fieldset class="fields-wrapper">
<label for="path">Path</label> <TextBox label="Path" readonly id="path" value={project?.path} />
<TextBox readonly id="path" value={project?.path} /> <section class="description-wrapper">
</div>
<div class="flex flex-col gap-1">
<label for="name">Project Name</label>
<TextBox <TextBox
label="Project Name"
id="name" id="name"
placeholder="Project name can't be empty" placeholder="Project name can't be empty"
bind:value={title} bind:value={title}
@ -32,18 +32,32 @@
dispatch('updated', project); dispatch('updated', project);
}} }}
/> />
</div>
<div class="flex flex-col gap-1">
<label for="description">Project Description</label>
<TextArea <TextArea
id="description" id="description"
rows={3} rows={3}
placeholder="Project description"
bind:value={description} bind:value={description}
on:change={(e) => { on:change={(e) => {
project.description = e.detail; project.description = e.detail;
dispatch('updated', project); dispatch('updated', project);
}} }}
/> />
</div> </section>
</fieldset> </fieldset>
</form> </form>
</SectionCard>
<Spacer />
<style>
.fields-wrapper {
display: flex;
flex-direction: column;
gap: var(--space-16);
}
.description-wrapper {
display: flex;
flex-direction: column;
gap: var(--space-8);
}
</style>

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import Button from './Button.svelte';
import HunkViewer from './HunkViewer.svelte'; import HunkViewer from './HunkViewer.svelte';
import Icon from './Icon.svelte'; import Icon from './Icon.svelte';
import { computeAddedRemovedByHunk } from '$lib/utils/metrics'; import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
@ -30,6 +31,8 @@
$: maxLineNumber = sections[sections.length - 1]?.maxLineNumber; $: maxLineNumber = sections[sections.length - 1]?.maxLineNumber;
$: minWidth = getGutterMinWidth(maxLineNumber); $: minWidth = getGutterMinWidth(maxLineNumber);
let alwaysShow = false;
</script> </script>
<div class="hunks"> <div class="hunks">
@ -37,6 +40,13 @@
Binary content not shown Binary content not shown
{:else if isLarge} {:else if isLarge}
Diff too large to be shown Diff too large to be shown
{:else if sections.length > 50 && !alwaysShow}
<div class="flex flex-col p-1">
Change hidden as large numbers of diffs may slow down the UI
<Button kind="outlined" color="neutral" on:click={() => (alwaysShow = true)}
>show anyways</Button
>
</div>
{:else} {:else}
{#each sections as section} {#each sections as section}
{@const { added, removed } = computeAddedRemovedByHunk(section)} {@const { added, removed } = computeAddedRemovedByHunk(section)}

View File

@ -50,7 +50,25 @@
}); });
</script> </script>
<div <div class="list-item-wrapper">
{#if showCheckbox}
<Checkbox
small
{checked}
{indeterminate}
on:change={(e) => {
selectedOwnership.update((ownership) => {
if (e.detail) file.hunks.forEach((h) => ownership.addHunk(file.id, h.id));
if (!e.detail) file.hunks.forEach((h) => ownership.removeHunk(file.id, h.id));
return ownership;
});
}}
/>
{/if}
<div
class="file-list-item"
id={`file-${file.id}`}
class:selected-draggable={selected}
on:click on:click
on:keydown on:keydown
on:dragstart={() => { on:dragstart={() => {
@ -70,29 +88,8 @@
popupMenu.openByMouse(e, { popupMenu.openByMouse(e, {
files: $selectedFiles.includes(file) ? $selectedFiles : [file] files: $selectedFiles.includes(file) ? $selectedFiles : [file]
})} })}
>
<div
class="file-list-item"
id={`file-${file.id}`}
class:selected-draggable={selected}
role="listitem"
on:contextmenu|preventDefault
> >
<div class="info-wrap"> <div class="info-wrap">
{#if showCheckbox}
<Checkbox
small
{checked}
{indeterminate}
on:change={(e) => {
selectedOwnership.update((ownership) => {
if (e.detail) file.hunks.forEach((h) => ownership.addHunk(file.id, h.id));
if (!e.detail) file.hunks.forEach((h) => ownership.removeHunk(file.id, h.id));
return ownership;
});
}}
/>
{/if}
<div class="info"> <div class="info">
<img src={getVSIFileIcon(file.path)} alt="js" style="width: var(--space-12)" /> <img src={getVSIFileIcon(file.path)} alt="js" style="width: var(--space-12)" />
<span class="text-base-12 name"> <span class="text-base-12 name">
@ -108,7 +105,14 @@
</div> </div>
<style lang="postcss"> <style lang="postcss">
.list-item-wrapper {
display: flex;
align-items: center;
gap: var(--space-8);
}
.file-list-item { .file-list-item {
flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
height: var(--space-28); height: var(--space-28);

View File

@ -0,0 +1,15 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
</script>
<div class="loading" data-tauri-drag-region><Icon name="spinner" /></div>
<style>
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
</style>

View File

@ -1,4 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { pxToRem } from '$lib/utils/pxToRem';
export type IconColor = 'success' | 'error' | 'pop' | 'warn' | 'neutral' | undefined; export type IconColor = 'success' | 'error' | 'pop' | 'warn' | 'neutral' | undefined;
</script> </script>
@ -10,8 +11,6 @@
export let opacity: number | undefined = 1; export let opacity: number | undefined = 1;
export let spinnerRadius: number | undefined = 5; export let spinnerRadius: number | undefined = 5;
export let size = 16; export let size = 16;
const pxToRem = (px: number) => `${px / 16}rem`;
</script> </script>
<svg <svg

View File

@ -32,9 +32,9 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--clr-theme-scale-ntrl-40);
border-radius: var(--radius-m); border-radius: var(--radius-m);
color: var(--clr-theme-scale-ntrl-50); color: var(--clr-theme-scale-ntrl-50);
cursor: pointer;
transition: transition:
background-color var(--transition-fast), background-color var(--transition-fast),
color var(--transition-fast); color var(--transition-fast);
@ -45,6 +45,7 @@
} }
.selected { .selected {
background-color: color-mix(in srgb, transparent, var(--darken-tint-light)); background-color: color-mix(in srgb, transparent, var(--darken-tint-light));
cursor: default;
} }
.large { .large {
height: var(--size-btn-l); height: var(--size-btn-l);

View File

@ -125,4 +125,8 @@
:global(.info-message__text p:not(:last-child)) { :global(.info-message__text p:not(:last-child)) {
margin-bottom: var(--space-10); margin-bottom: var(--space-10);
} }
:global(.info-message__text ul) {
list-style-type: circle;
padding: 0 0 0 var(--space-16);
}
</style> </style>

View File

@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
import ClickableCard from './ClickableCard.svelte';
import RadioButton from './RadioButton.svelte';
import SectionCard from './SectionCard.svelte';
import Spacer from './Spacer.svelte';
import TextBox from './TextBox.svelte';
import { invoke } from '$lib/backend/ipc'; import { invoke } from '$lib/backend/ipc';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import Link from '$lib/components/Link.svelte'; import Link from '$lib/components/Link.svelte';
import TextBox from '$lib/components/TextBox.svelte';
import { copyToClipboard } from '$lib/utils/clipboard'; import { copyToClipboard } from '$lib/utils/clipboard';
import { debounce } from '$lib/utils/debounce'; import { debounce } from '$lib/utils/debounce';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@ -81,121 +85,199 @@
preferred_key: 'generated' preferred_key: 'generated'
}); });
} }
let showPassphrase = false;
</script> </script>
<div class="flex flex-col gap-1"> <section class="git-auth-wrap">
<p>Git Authentication</p> <h3 class="text-base-15 text-bold">Git Authentication</h3>
<div class="pr-8 text-sm text-light-700 dark:text-dark-200"> <p class="text-base-body-12">
<div>
Configure the authentication flow for GitButler when authenticating with your Git remote Configure the authentication flow for GitButler when authenticating with your Git remote
provider. provider.
</div> </p>
</div>
<div class="grid grid-cols-2 gap-2" style="grid-template-columns: max-content 1fr;"> <form>
<input type="radio" bind:group={selectedOption} value="default" on:input={setDefaultKey} /> <fieldset class="git-radio">
<div class="flex flex-col space-y-2"> <ClickableCard
<div>Auto detect</div> hasBottomRadius={false}
{#if selectedOption === 'default'} on:click={() => {
<div class="pr-8 text-sm text-light-700 dark:text-dark-200"> if (selectedOption == 'default') return;
<div>GitButler will attempt all available authentication flows automatically.</div>
</div>
{/if}
</div>
<input type="radio" bind:group={selectedOption} value="local" /> selectedOption = 'default';
<div class="flex flex-col space-y-2"> setDefaultKey();
<div>Use existing SSH key</div> }}
>
<svelte:fragment slot="title">Auto detect</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="default" on:input={setDefaultKey} />
</svelte:fragment>
<svelte:fragment slot="body">
GitButler will attempt all available authentication flows automatically.
</svelte:fragment>
</ClickableCard>
<ClickableCard
hasTopRadius={false}
hasBottomRadius={false}
hasBottomLine={selectedOption !== 'local'}
on:click={() => {
selectedOption = 'local';
}}
>
<svelte:fragment slot="title">Use existing SSH key</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="local" />
</svelte:fragment>
<svelte:fragment slot="body">
Add the path to an existing SSH key that GitButler can use.
</svelte:fragment>
</ClickableCard>
{#if selectedOption === 'local'} {#if selectedOption === 'local'}
<div class="pr-8 text-sm text-light-700 dark:text-dark-200"> <SectionCard hasTopRadius={false} hasBottomRadius={false}>
Add the path to an existing SSH key that GitButler can use. <div class="inputs-group">
</div>
<div
class="grid grid-cols-2 items-center gap-2"
style="grid-template-columns: max-content 1fr;"
>
<label for="path">Path to private key</label>
<TextBox <TextBox
label="Path to private key"
placeholder="for example: ~/.ssh/id_rsa" placeholder="for example: ~/.ssh/id_rsa"
bind:value={privateKeyPath} bind:value={privateKeyPath}
on:input={debounce(setLocalKey, 600)} on:input={debounce(setLocalKey, 600)}
/> />
<label for="passphrase">Passphrase (optional)</label> <div class="input-with-button">
<TextBox <TextBox
type="password" label="Passphrase (optional)"
type={showPassphrase ? 'text' : 'password'}
bind:value={privateKeyPassphrase} bind:value={privateKeyPassphrase}
on:input={debounce(setLocalKey, 600)} on:input={debounce(setLocalKey, 600)}
wide
/> />
<Button
size="large"
color="neutral"
kind="outlined"
icon={showPassphrase ? 'eye-shown' : 'eye-hidden'}
on:click={() => (showPassphrase = !showPassphrase)}
width={150}
>
{showPassphrase ? 'Hide passphrase' : 'Show passphrase'}
</Button>
</div> </div>
</div>
</SectionCard>
{/if} {/if}
</div>
<input type="radio" bind:group={selectedOption} value="generated" on:input={setGeneratedKey} /> <ClickableCard
<div class="flex flex-col space-y-2"> hasTopRadius={false}
<div class="pr-8"> hasBottomRadius={false}
<div>Use locally generated SSH key</div> hasBottomLine={selectedOption !== 'generated'}
</div> on:click={() => {
if (selectedOption == 'generated') return;
selectedOption = 'generated';
setGeneratedKey();
}}
>
<svelte:fragment slot="title">Use locally generated SSH key</svelte:fragment>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="generated" />
</svelte:fragment>
<svelte:fragment slot="body">
GitButler will use a locally generated SSH key. For this to work you need to add the
following public key to your Git remote provider:
</svelte:fragment>
</ClickableCard>
{#if selectedOption === 'generated'} {#if selectedOption === 'generated'}
<div class="pr-8 text-sm text-light-700 dark:text-dark-200"> <SectionCard hasTopRadius={false} hasBottomRadius={false}>
GitButler will use a locally generated SSH key. For this to work you <b>need</b> <TextBox readonly selectall bind:value={sshKey} />
to add the following public key to your Git remote provider: <div class="row-buttons">
</div>
<div class="flex-auto overflow-y-scroll">
<input
bind:value={sshKey}
disabled={selectedOption !== 'generated'}
class="whitespece-pre input w-full select-all rounded border p-2 font-mono"
/>
</div>
<div class="flex flex-row justify-end space-x-2">
<div>
<Button <Button
kind="filled" kind="filled"
color="primary" color="primary"
icon="copy"
on:click={() => copyToClipboard(sshKey)} on:click={() => copyToClipboard(sshKey)}
disabled={selectedOption !== 'generated'}
> >
Copy to Clipboard Copy to Clipboard
</Button> </Button>
</div> <Button
<div class="p-1"> kind="outlined"
<Link color="neutral"
target="_blank" icon="open-link"
rel="noreferrer" on:click={() => {
href="https://github.com/settings/ssh/new" open('https://github.com/settings/ssh/new');
disabled={selectedOption !== 'generated'} }}
> >
Add key to GitHub Add key to GitHub
</Link> </Button>
</div>
</div> </div>
</SectionCard>
{/if} {/if}
</div>
<input <ClickableCard
type="radio" hasTopRadius={false}
bind:group={selectedOption} on:click={() => {
value="gitCredentialsHelper" if (selectedOption == 'gitCredentialsHelper') return;
on:input={setGitCredentialsHelperKey}
/> selectedOption = 'gitCredentialsHelper';
<div class="flex flex-col space-y-2"> setGitCredentialsHelperKey();
<div class="pr-8"> }}
<div>
Use a
<Link target="_blank" rel="noreferrer" href="https://git-scm.com/doc/credential-helpers"
>Git credentials helper</Link
> >
</div> <svelte:fragment slot="title">Use a Git credentials helper</svelte:fragment>
</div>
{#if selectedOption === 'gitCredentialsHelper'} <svelte:fragment slot="body">
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
GitButler will use the system's git credentials helper. GitButler will use the system's git credentials helper.
</div> <Link target="_blank" rel="noreferrer" href="https://git-scm.com/doc/credential-helpers">
{/if} Learn more
</div> </Link>
</div> </svelte:fragment>
</div>
<svelte:fragment slot="actions">
<RadioButton bind:group={selectedOption} value="gitCredentialsHelper" />
</svelte:fragment>
</ClickableCard>
</fieldset>
</form>
</section>
<Spacer />
<style>
.git-auth-wrap {
display: flex;
flex-direction: column;
gap: var(--space-16);
& p {
color: var(--clr-theme-scale-ntrl-30);
}
}
.inputs-group {
display: flex;
flex-direction: column;
gap: var(--space-16);
}
.input-with-button {
display: flex;
gap: var(--space-8);
align-items: flex-end;
}
.row-buttons {
display: flex;
justify-content: flex-end;
gap: var(--space-8);
}
.git-radio {
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
import Checkbox from '$lib/components/Checkbox.svelte'; import Spacer from './Spacer.svelte';
import ClickableCard from '$lib/components/ClickableCard.svelte';
import Toggle from '$lib/components/Toggle.svelte';
import { projectRunCommitHooks } from '$lib/config/config'; import { projectRunCommitHooks } from '$lib/config/config';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import type { Project } from '$lib/backend/projects'; import type { Project } from '$lib/backend/projects';
@ -16,57 +18,70 @@
omit_certificate_check?: boolean; omit_certificate_check?: boolean;
}; };
}>(); }>();
const onAllowForcePushingChange = () => {
dispatch('updated', { ok_with_force_push: allowForcePushing });
};
const onOmitCertificateCheckChange = () => {
dispatch('updated', { omit_certificate_check: omitCertificateCheck });
};
const onRunCommitHooksChange = () => {
$runCommitHooks = !$runCommitHooks;
};
</script> </script>
<div class="flex flex-1 flex-col gap-1 rounded-lg border border-light-400 p-2 dark:border-dark-500"> <section class="wrapper">
<form class="flex items-center gap-1"> <ClickableCard
<Checkbox on:click={() => {
name="allow-force-pushing"
checked={allowForcePushing}
on:change={() => {
allowForcePushing = !allowForcePushing; allowForcePushing = !allowForcePushing;
dispatch('updated', { ok_with_force_push: allowForcePushing }); onAllowForcePushingChange();
}} }}
/> >
<label class="ml-2" for="allow-force-pushing"> <svelte:fragment slot="title">Allow force pushing</svelte:fragment>
<div>Allow force pushing</div> <svelte:fragment slot="body">
</label> Force pushing allows GitButler to override branches even if they were pushed to remote. We
</form> will never force push to the trunk.
<p class="ml-7 text-light-700 dark:text-dark-200"> </svelte:fragment>
Force pushing allows GitButler to override branches even if they were pushed to remote. We will <svelte:fragment slot="actions">
never force push to the trunk. <Toggle bind:checked={allowForcePushing} on:change={onAllowForcePushingChange} />
</p> </svelte:fragment>
</ClickableCard>
<form class="flex items-center gap-1"> <ClickableCard
<Checkbox on:click={() => {
name="omit-certificate-check"
checked={omitCertificateCheck}
on:change={() => {
omitCertificateCheck = !omitCertificateCheck; omitCertificateCheck = !omitCertificateCheck;
dispatch('updated', { omit_certificate_check: omitCertificateCheck }); onOmitCertificateCheckChange();
}} }}
/> >
<label class="ml-2" for="allow-force-pushing"> <svelte:fragment slot="title">Ignore host certificate checks</svelte:fragment>
<div>Ignore host certificate checks</div> <svelte:fragment slot="body">
</label>
</form>
<p class="ml-7 text-light-700 dark:text-dark-200">
Enabling this will ignore host certificate checks when authenticating with ssh. Enabling this will ignore host certificate checks when authenticating with ssh.
</p> </svelte:fragment>
<svelte:fragment slot="actions">
<Toggle bind:checked={omitCertificateCheck} on:change={onOmitCertificateCheckChange} />
</svelte:fragment>
</ClickableCard>
<form class="flex items-center gap-1"> <ClickableCard on:click={onRunCommitHooksChange}>
<Checkbox <svelte:fragment slot="title">Run commit hooks</svelte:fragment>
name="run-commit-hooks" <svelte:fragment slot="body">
checked={$runCommitHooks} Enabling this will run any git pre and post commit hooks you have configured in your
on:change={() => { repository.
$runCommitHooks = !$runCommitHooks; </svelte:fragment>
}} <svelte:fragment slot="actions">
/> <Toggle bind:checked={$runCommitHooks} on:change={onRunCommitHooksChange} />
<label class="ml-2" for="allow-force-pushing"> </svelte:fragment>
<div>Run commit hooks</div> </ClickableCard>
</label> </section>
</form>
<p class="ml-7 text-light-700 dark:text-dark-200"> <Spacer />
Enabling this will run any git pre and post commit hooks you have configured in your repository.
</p> <style lang="post-css">
</div> .wrapper {
display: flex;
flex-direction: column;
gap: var(--space-8);
}
</style>

View File

@ -29,10 +29,14 @@
(b) => b.name == 'origin/master' || b.name == 'origin/main' (b) => b.name == 'origin/master' || b.name == 'origin/main'
); );
function onSetTargetClick() { async function onSetTargetClick() {
if (!selectedBranch) return; if (!selectedBranch) return;
loading = true; loading = true;
branchController.setTarget(selectedBranch.name).finally(() => (loading = false)); try {
await branchController.setTarget(selectedBranch.name);
} finally {
loading = false;
}
} }
</script> </script>

View File

@ -22,6 +22,7 @@
<style lang="postcss"> <style lang="postcss">
.radio { .radio {
appearance: none; appearance: none;
cursor: pointer;
width: var(--space-16); width: var(--space-16);
height: var(--space-16); height: var(--space-16);
border-radius: var(--space-16); border-radius: var(--space-16);

View File

@ -25,10 +25,10 @@
modal.show(); modal.show();
}} }}
> >
Remove project Remove project
</Button> </Button>
<Modal bind:this={modal} title="Remove from GitButler"> <Modal bind:this={modal}>
<div class="remove-project-description"> <div class="remove-project-description">
<p class="text-base-body-14"> <p class="text-base-body-14">
Are you sure you want to remove Are you sure you want to remove

View File

@ -1,9 +1,19 @@
<script lang="ts"> <script lang="ts">
export let orientation: 'row' | 'column' = 'column'; export let orientation: 'row' | 'column' = 'column';
export let hasTopRadius = true;
export let hasBottomRadius = true;
export let hasBottomLine = true;
const SLOTS = $$props.$$slots; const SLOTS = $$props.$$slots;
</script> </script>
<section class="section-card" style:flex-direction={orientation}> <section
class="section-card"
style:flex-direction={orientation}
class:has-top-radius={hasTopRadius}
class:has-bottom-radius={hasBottomRadius}
class:has-bottom-line={hasBottomLine}
>
{#if SLOTS.iconSide} {#if SLOTS.iconSide}
<div class="section-card__icon-side"> <div class="section-card__icon-side">
<slot name="iconSide" /> <slot name="iconSide" />
@ -32,8 +42,8 @@
display: flex; display: flex;
gap: var(--space-16); gap: var(--space-16);
padding: var(--space-16); padding: var(--space-16);
border-radius: var(--radius-m); border-left: 1px solid var(--clr-theme-container-outline-light);
border: 1px solid var(--clr-theme-container-outline-light); border-right: 1px solid var(--clr-theme-container-outline-light);
background-color: var(--clr-theme-container-light); background-color: var(--clr-theme-container-light);
transition: transition:
background-color var(--transition-fast), background-color var(--transition-fast),
@ -55,4 +65,21 @@
.section-card__text { .section-card__text {
color: var(--clr-theme-scale-ntrl-30); color: var(--clr-theme-scale-ntrl-30);
} }
/* MODIFIERS */
.has-top-radius {
border-top: 1px solid var(--clr-theme-container-outline-light);
border-top-left-radius: var(--radius-m);
border-top-right-radius: var(--radius-m);
}
.has-bottom-radius {
border-bottom-left-radius: var(--radius-m);
border-bottom-right-radius: var(--radius-m);
}
.has-bottom-line {
border-bottom: 1px solid var(--clr-theme-container-outline-light);
}
</style> </style>

View File

@ -84,6 +84,8 @@
transition: background var(--transition-fast); transition: background var(--transition-fast);
cursor: pointer;
&:hover { &:hover {
background-color: color-mix( background-color: color-mix(
in srgb, in srgb,
@ -98,11 +100,15 @@
border-right-width: 1px; border-right-width: 1px;
border-left-width: 1px; border-left-width: 1px;
cursor: default;
& > .label { & > .label {
color: var(--clr-theme-scale-ntrl-0); color: var(--clr-theme-scale-ntrl-0);
cursor: default;
} }
& > .icon { & > .icon {
color: var(--clr-theme-scale-ntrl-30); color: var(--clr-theme-scale-ntrl-30);
cursor: default;
} }
&.left { &.left {
border-right-width: 1px; border-right-width: 1px;
@ -128,9 +134,11 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: var(--clr-theme-scale-ntrl-50); color: var(--clr-theme-scale-ntrl-50);
cursor: pointer;
} }
.label { .label {
color: var(--clr-theme-scale-ntrl-40); color: var(--clr-theme-scale-ntrl-40);
cursor: pointer;
} }
</style> </style>

View File

@ -16,6 +16,7 @@
.divider { .divider {
height: 1px; height: 1px;
width: 100%; width: 100%;
border-bottom: 1px solid var(--clr-theme-container-outline-light); border-bottom: 1px solid var(--clr-theme-scale-ntrl-0);
opacity: 0.15;
} }
</style> </style>

View File

@ -17,6 +17,7 @@
<button <button
class="sync-btn" class="sync-btn"
class:sync-btn-busy={$baseServiceBusy$}
on:click={async (e) => { on:click={async (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -53,6 +54,12 @@
background: var(--clr-theme-container-light); background: var(--clr-theme-container-light);
border: 1px solid var(--clr-theme-container-outline-light); border: 1px solid var(--clr-theme-container-outline-light);
border-radius: var(--radius-m); border-radius: var(--radius-m);
cursor: pointer;
&.sync-btn-busy {
cursor: default;
}
transition: transition:
background var(--transition-fast), background var(--transition-fast),
border var(--transition-fast); border var(--transition-fast);

View File

@ -79,7 +79,7 @@
padding: 0 var(--space-2); padding: 0 var(--space-2);
} }
.clickable { .clickable {
cursor: default; cursor: pointer;
} }
/* colors */ /* colors */

View File

@ -10,12 +10,19 @@
export let autocomplete: string | undefined = undefined; export let autocomplete: string | undefined = undefined;
export let autocorrect: string | undefined = undefined; export let autocorrect: string | undefined = undefined;
export let spellcheck = false; export let spellcheck = false;
export let label: string | undefined = undefined;
const dispatch = createEventDispatcher<{ input: string; change: string }>(); const dispatch = createEventDispatcher<{ input: string; change: string }>();
</script> </script>
<textarea <div class="textarea-wrapper">
class="text-input textarea" {#if label}
<label class="textbox__label text-base-13 text-semibold" for={id}>
{label}
</label>
{/if}
<textarea
class="text-input text-base-body-13 textarea"
bind:value bind:value
{disabled} {disabled}
{id} {id}
@ -28,13 +35,24 @@
{spellcheck} {spellcheck}
on:input={(e) => dispatch('input', e.currentTarget.value)} on:input={(e) => dispatch('input', e.currentTarget.value)}
on:change={(e) => dispatch('change', e.currentTarget.value)} on:change={(e) => dispatch('change', e.currentTarget.value)}
/> />
</div>
<style lang="postcss"> <style lang="postcss">
.textarea-wrapper {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-6);
}
.textarea { .textarea {
width: 100%; width: 100%;
resize: none; resize: none;
padding-top: var(--space-12); padding-top: var(--space-12);
padding-bottom: var(--space-12); padding-bottom: var(--space-12);
} }
.textbox__label {
color: var(--clr-theme-scale-ntrl-50);
}
</style> </style>

View File

@ -3,27 +3,29 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import type iconsJson from '$lib/icons/icons.json'; import type iconsJson from '$lib/icons/icons.json';
export let element: HTMLElement | undefined = undefined;
export let id: string | undefined = undefined; // Required to make label clickable export let id: string | undefined = undefined; // Required to make label clickable
export let icon: keyof typeof iconsJson | undefined = undefined; export let icon: keyof typeof iconsJson | undefined = undefined;
export let value: string | undefined = undefined; export let value: string | undefined = undefined;
export let placeholder: string | undefined = undefined; export let placeholder: string | undefined = undefined;
export let label: string | undefined = undefined; export let label: string | undefined = undefined;
export let reversedDirection: boolean = false; export let reversedDirection: boolean = false;
export let wide: boolean = false;
export let disabled = false; export let disabled = false;
export let readonly = false; export let readonly = false;
export let required = false; export let required = false;
export let noselect = false; export let noselect = false;
export let selectall = false; export let selectall = false;
export let element: HTMLElement | undefined = undefined;
export let spellcheck = false; export let spellcheck = false;
export let type: 'text' | 'password' | 'select' = 'text'; export let type: 'text' | 'password' | 'select' = 'text';
const dispatch = createEventDispatcher<{ input: string; change: string }>(); const dispatch = createEventDispatcher<{ input: string; change: string }>();
</script> </script>
<div class="textbox" bind:this={element}> <div class="textbox" bind:this={element} class:wide>
{#if label} {#if label}
<label class="textbox__label font-base-13 text-semibold" for={id}> <label class="textbox__label text-base-13 text-semibold" for={id}>
{label} {label}
</label> </label>
{/if} {/if}
@ -38,6 +40,7 @@
<Icon name={icon} /> <Icon name={icon} />
</div> </div>
{/if} {/if}
<input <input
{id} {id}
{readonly} {readonly}
@ -47,7 +50,7 @@
{disabled} {disabled}
{...{ type }} {...{ type }}
class="text-input textbox__input text-base-13" class="text-input textbox__input text-base-13"
class:textbox__readonly={type !== 'select' && readonly} class:textbox__readonly={type != 'select' && readonly}
class:select-none={noselect} class:select-none={noselect}
class:select-all={selectall} class:select-all={selectall}
bind:value bind:value
@ -71,6 +74,8 @@
} }
.textbox__input { .textbox__input {
z-index: 1;
position: relative;
flex-grow: 1; flex-grow: 1;
height: var(--size-btn-l); height: var(--size-btn-l);
width: 100%; width: 100%;
@ -82,11 +87,16 @@
} }
} }
.textbox__input[type='select'] {
cursor: pointer;
}
.textbox__label { .textbox__label {
color: var(--clr-theme-scale-ntrl-50); color: var(--clr-theme-scale-ntrl-50);
} }
.textbox__icon { .textbox__icon {
z-index: 2;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -118,4 +128,9 @@
background-color: var(--clr-theme-container-pale); background-color: var(--clr-theme-container-pale);
border-color: var(--clr-theme-container-outline-light); border-color: var(--clr-theme-container-outline-light);
} }
.wide {
width: 100%;
flex: 1;
}
</style> </style>

View File

@ -58,6 +58,7 @@
} }
.theme-card { .theme-card {
cursor: pointer;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -78,14 +79,14 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: auto; height: auto;
border-radius: var(--radius-l); border-radius: var(--radius-m);
border: 1px solid var(--clr-theme-container-outline-light); border: 1px solid var(--clr-theme-container-outline-light);
overflow: hidden; overflow: hidden;
& img { & img {
width: 100%; width: 100%;
height: 100%; height: auto;
object-fit: cover; border-radius: var(--radius-m);
} }
} }
@ -114,7 +115,10 @@
.theme-card.selected .theme-card__preview { .theme-card.selected .theme-card__preview {
border-color: var(--clr-core-pop-50); border-color: var(--clr-core-pop-50);
border-width: 2px; }
.theme-card.selected .theme-card__label {
background-color: color-mix(in srgb, var(--clr-theme-scale-pop-50), transparent 80%);
} }
.theme-card.selected .theme-card__icon { .theme-card.selected .theme-card__icon {

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { tooltip } from '$lib/utils/tooltip'; import { tooltip } from '$lib/utils/tooltip';
import { createEventDispatcher } from 'svelte';
export let name = ''; export let name = '';
@ -8,12 +9,18 @@
export let checked = false; export let checked = false;
export let value = ''; export let value = '';
export let help = ''; export let help = '';
let input: HTMLInputElement;
const dispatch = createEventDispatcher<{ change: boolean }>();
</script> </script>
<input <input
bind:this={input}
bind:checked
on:click|stopPropagation on:click|stopPropagation
on:change on:change={() => {
use:tooltip={help} dispatch('change', checked);
}}
type="checkbox" type="checkbox"
class="toggle" class="toggle"
class:small class:small
@ -21,12 +28,13 @@
{name} {name}
id={name} id={name}
{disabled} {disabled}
bind:checked use:tooltip={help}
/> />
<style lang="postcss"> <style lang="postcss">
.toggle { .toggle {
appearance: none; appearance: none;
cursor: pointer;
width: calc(var(--space-24) + var(--space-2)); width: calc(var(--space-24) + var(--space-2));
height: var(--space-16); height: var(--space-16);
border-radius: var(--space-16); border-radius: var(--space-16);

View File

@ -0,0 +1,311 @@
<script lang="ts">
import Button from './Button.svelte';
import IconButton from './IconButton.svelte';
import { showToast } from '$lib/notifications/toasts';
import { relaunch } from '@tauri-apps/api/process';
import { installUpdate } from '@tauri-apps/api/updater';
import { distinctUntilChanged, tap } from 'rxjs';
import { fade } from 'svelte/transition';
import type { UpdaterService } from '$lib/backend/updater';
export let updaterService: UpdaterService;
// Extrend update stream to allow dismissing updater by version
$: update$ = updaterService.update$.pipe(
// Only run operators after this one once per version
distinctUntilChanged((prev, curr) => prev?.version == curr?.version),
// Reset dismissed boolean when a new version becomes available
tap(() => (dismissed = false))
);
let dismissed = false;
</script>
{#if $update$?.version && $update$.status != 'UPTODATE' && !dismissed}
<div class="update-banner" class:busy={$update$?.status == 'PENDING'}>
<div class="floating-button">
<IconButton icon="cross-small" on:click={() => (dismissed = true)} />
</div>
<div class="img">
<div class="circle-img">
{#if $update$?.status != 'DONE'}
<svg
class="arrow-img"
width="12"
height="34"
viewBox="0 0 12 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 21V32.5M6 32.5L1 27.5M6 32.5L11 27.5"
stroke="var(--clr-theme-scale-ntrl-100)"
stroke-width="1.5"
/>
<path
d="M6 0V11.5M6 11.5L1 6.5M6 11.5L11 6.5"
stroke="var(--clr-theme-scale-ntrl-100)"
stroke-width="1.5"
/>
</svg>
{:else}
<svg
class="tick-img"
width="14"
height="11"
viewBox="0 0 14 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 4.07692L5.57143 9L13 1"
stroke="var(--clr-theme-scale-ntrl-100)"
stroke-width="1.5"
/>
</svg>
{/if}
</div>
<svg
width="60"
height="36"
viewBox="0 0 60 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M31.5605 35.5069C31.4488 35.5097 31.3368 35.5112 31.2245 35.5112H12.8571C5.75633 35.5112 0 29.7548 0 22.654C0 15.5532 5.75634 9.79688 12.8571 9.79688H16.123C18.7012 4.02354 24.493 0 31.2245 0C39.7331 0 46.7402 6.42839 47.6541 14.6934H49.5918C55.3401 14.6934 60 19.3533 60 25.1015C60 30.8498 55.3401 35.5097 49.5918 35.5097H32.4489C32.2692 35.5097 32.0906 35.5051 31.913 35.4961C31.7958 35.5009 31.6783 35.5045 31.5605 35.5069Z"
fill="var(--clr-theme-scale-pop-70)"
/>
<g opacity="0.4">
<path
d="M39 35.5102V29.2505H29.25V9.75049H39V19.5005H48.75V29.2505H58.5V30.4877C56.676 33.4983 53.3688 35.5102 49.5918 35.5102H39Z"
fill="var(--clr-theme-scale-pop-50)"
/>
<path
d="M46.3049 9.75049H39V1.93967C42.2175 3.65783 44.8002 6.4091 46.3049 9.75049Z"
fill="var(--clr-theme-scale-pop-50)"
/>
<path
d="M9.75 35.1337C10.745 35.3806 11.7858 35.5117 12.8571 35.5117H29.25V29.2505H9.75V19.5005H19.5V9.75049H29.25V0.117188C25.4568 0.568673 22.0577 2.30464 19.5 4.87786V9.75049H16.144C16.137 9.7661 16.13 9.78173 16.123 9.79737H12.8571C11.7858 9.79737 10.745 9.92841 9.75 10.1753V19.5005H0.389701C0.135193 20.5097 0 21.5663 0 22.6545C0 25.0658 0.663785 27.322 1.81859 29.2505H9.75V35.1337Z"
fill="var(--clr-theme-scale-pop-50)"
/>
</g>
</svg>
</div>
<h4 class="text-base-13 label">
{#if !$update$.status}
New version available
{:else if $update$.status == 'PENDING'}
Downloading update...
{:else if $update$.status == 'DONE'}
Installing update...
{:else if $update$.status == 'ERROR'}
Error occurred...
{/if}
</h4>
<div class="buttons">
<Button
wide
kind="outlined"
on:click={() => {
const notes = $update$?.body?.trim() || 'no release notes available';
showToast({
id: 'release-notes',
title: `Release notes for ${$update$?.version}`,
message: `
${notes}
`
});
}}
>
Release notes
</Button>
<div class="status-section">
<div class="sliding-gradient" />
{#if !$update$.status}
<div class="cta-btn" transition:fade={{ duration: 100 }}>
<Button wide on:click={() => installUpdate()}>Download {$update$.version}</Button>
</div>
{:else if $update$.status == 'DONE'}
<div class="cta-btn" transition:fade={{ duration: 100 }}>
<Button wide on:click={() => relaunch()}>Restart to update</Button>
</div>
{/if}
</div>
</div>
</div>
{/if}
<style lang="postcss">
.update-banner {
cursor: default;
user-select: none;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-16);
width: 100%;
max-width: 220px;
position: fixed;
bottom: var(--space-12);
left: var(--space-12);
padding: var(--space-24);
background-color: var(--clr-theme-container-light);
border: 1px solid var(--clr-theme-container-outline-light);
border-radius: var(--radius-m);
}
.label {
color: var(--clr-theme-scale-ntrl-0);
}
.buttons {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--space-8);
}
/* STATUS SECTION */
.status-section {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
height: var(--size-btn-m);
width: 100%;
background-color: var(--clr-theme-pop-element);
border-radius: var(--radius-m);
transition:
transform 0.15s ease-in-out,
height 0.15s ease-in-out;
}
.sliding-gradient {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 100%;
mix-blend-mode: overlay;
background: linear-gradient(
80deg,
rgba(255, 255, 255, 0) 9%,
rgba(255, 255, 255, 0.5) 31%,
rgba(255, 255, 255, 0) 75%
);
animation: slide 3s ease-in-out infinite;
transition: width 0.2s ease-in-out;
}
@keyframes slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.cta-btn {
display: flex;
width: 100%;
position: relative;
}
.busy {
& .status-section {
height: var(--space-4);
}
& .sliding-gradient {
width: 100%;
background: linear-gradient(
80deg,
rgba(255, 255, 255, 0) 9%,
rgba(255, 255, 255, 0.9) 31%,
rgba(255, 255, 255, 0) 75%
);
animation: slide 1.6s ease-in infinite;
}
& .arrow-img {
transform: rotate(180deg);
animation: moving-arrow 1s ease-in-out infinite;
}
}
/* IMAGE */
.img {
position: relative;
margin-bottom: 4px;
}
.circle-img {
position: absolute;
overflow: hidden;
bottom: -8px;
left: 17px;
width: 26px;
height: 26px;
border-radius: 50%;
background-color: var(--clr-theme-scale-pop-40);
transition: transform 0.2s ease-in-out;
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: transparent;
box-shadow: inset 0 0 4px 4px var(--clr-theme-scale-pop-40);
border-radius: 50%;
}
}
.arrow-img {
position: absolute;
top: -14px;
left: 7px;
/* transform: translateY(20px); */
}
.tick-img {
position: absolute;
top: 8px;
left: 6px;
}
.floating-button {
position: absolute;
right: var(--space-10);
top: var(--space-10);
}
@keyframes moving-arrow {
0% {
transform: translateY(0);
}
100% {
transform: translateY(21px);
}
}
</style>

View File

@ -31,14 +31,15 @@
<Icon name={icon} /> <Icon name={icon} />
{/if} {/if}
<div class="label text-base-12"> <span class="label text-base-12">
{label} {label}
</div> </span>
<slot name="control" /> <slot name="control" />
</button> </button>
<style lang="postcss"> <style lang="postcss">
.menu-item { .menu-item {
cursor: pointer;
display: flex; display: flex;
text-align: left; text-align: left;
align-items: center; align-items: center;
@ -54,6 +55,7 @@
} }
} }
.label { .label {
user-select: none;
flex-grow: 1; flex-grow: 1;
white-space: nowrap; white-space: nowrap;
} }

View File

@ -6,6 +6,7 @@
<section class="content-wrapper"> <section class="content-wrapper">
<ScrollableContainer> <ScrollableContainer>
<div class="drag-region" data-tauri-drag-region>
<div class="content" data-tauri-drag-region> <div class="content" data-tauri-drag-region>
{#if title} {#if title}
<h1 class="title text-head-24"> <h1 class="title text-head-24">
@ -14,6 +15,7 @@
{/if} {/if}
<slot /> <slot />
</div> </div>
</div>
</ScrollableContainer> </ScrollableContainer>
</section> </section>
@ -23,7 +25,12 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
flex: 1; flex: 1;
background-color: var(--clr-theme-container-light); background-color: var(--clr-theme-container-pale);
}
.drag-region {
width: 100%;
min-height: 100vh;
} }
.content { .content {

View File

@ -119,7 +119,7 @@
justify-content: space-between; justify-content: space-between;
padding: calc(var(--space-40) + var(--space-4)) var(--space-20) var(--space-20) var(--space-20); padding: calc(var(--space-40) + var(--space-4)) var(--space-20) var(--space-20) var(--space-20);
border-right: 1px solid var(--clr-theme-container-outline-light); border-right: 1px solid var(--clr-theme-container-outline-light);
background-color: var(--clr-theme-container-pale); background-color: var(--clr-theme-container-light);
height: 100%; height: 100%;
width: 16rem; width: 16rem;
} }
@ -172,7 +172,7 @@
transition: none; transition: none;
background-color: color-mix( background-color: color-mix(
in srgb, in srgb,
var(--clr-theme-container-pale), var(--clr-theme-container-light),
var(--darken-tint-light) var(--darken-tint-light)
); );
} }
@ -183,7 +183,11 @@
} }
.item_selected { .item_selected {
background-color: color-mix(in srgb, var(--clr-theme-container-pale), var(--darken-tint-light)); background-color: color-mix(
in srgb,
var(--clr-theme-container-light),
var(--darken-tint-light)
);
color: var(--clr-theme-scale-ntrl-0); color: var(--clr-theme-scale-ntrl-0);
} }
@ -209,7 +213,7 @@
padding: var(--space-16); padding: var(--space-16);
border-radius: var(--radius-m); border-radius: var(--radius-m);
border: 1px solid var(--clr-theme-container-outline-light); border: 1px solid var(--clr-theme-container-outline-light);
background-color: var(--clr-theme-container-pale); background-color: var(--clr-theme-container-light);
color: var(--clr-theme-scale-ntrl-30); color: var(--clr-theme-scale-ntrl-30);
transition: background-color var(--transition-fast); transition: background-color var(--transition-fast);

View File

@ -6,7 +6,7 @@ import {
type PrStatus, type PrStatus,
MergeMethod MergeMethod
} from '$lib/github/types'; } from '$lib/github/types';
import { showToast, type ToastMessage } from '$lib/notifications/toasts'; import { showToast, type Toast } from '$lib/notifications/toasts';
import { sleep } from '$lib/utils/sleep'; import { sleep } from '$lib/utils/sleep';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import lscache from 'lscache'; import lscache from 'lscache';
@ -496,7 +496,7 @@ function loadPrs(
* "status": 422 * "status": 422
* } * }
*/ */
function mapErrorToToast(err: any): ToastMessage | undefined { function mapErrorToToast(err: any): Toast | undefined {
// We expect an object to be thrown by octokit. // We expect an object to be thrown by octokit.
if (typeof err != 'object') return; if (typeof err != 'object') return;

View File

@ -84,5 +84,7 @@
"git": "M9.23744 1.17678C8.55402 0.49336 7.44598 0.493358 6.76257 1.17678L1.17678 6.76256C0.493362 7.44598 0.49336 8.55402 1.17678 9.23744L6.76257 14.8232C7.44598 15.5066 8.55402 15.5066 9.23744 14.8232L14.8232 9.23744C15.5066 8.55402 15.5066 7.44598 14.8232 6.76256L9.23744 1.17678ZM7.82323 2.23744C7.92086 2.13981 8.07915 2.13981 8.17678 2.23744L13.7626 7.82322C13.8602 7.92085 13.8602 8.07914 13.7626 8.17678L8.17678 13.7626C8.07915 13.8602 7.92086 13.8602 7.82323 13.7626L2.23744 8.17678C2.13981 8.07915 2.13981 7.92086 2.23744 7.82322L5.5 4.56066L7.43934 6.5L4.96967 8.96967L6.03033 10.0303L8.5 7.56066L10.4697 9.53033L11.5303 8.46967L9.03033 5.96967L6.56066 3.5L7.82323 2.23744Z", "git": "M9.23744 1.17678C8.55402 0.49336 7.44598 0.493358 6.76257 1.17678L1.17678 6.76256C0.493362 7.44598 0.49336 8.55402 1.17678 9.23744L6.76257 14.8232C7.44598 15.5066 8.55402 15.5066 9.23744 14.8232L14.8232 9.23744C15.5066 8.55402 15.5066 7.44598 14.8232 6.76256L9.23744 1.17678ZM7.82323 2.23744C7.92086 2.13981 8.07915 2.13981 8.17678 2.23744L13.7626 7.82322C13.8602 7.92085 13.8602 8.07914 13.7626 8.17678L8.17678 13.7626C8.07915 13.8602 7.92086 13.8602 7.82323 13.7626L2.23744 8.17678C2.13981 8.07915 2.13981 7.92086 2.23744 7.82322L5.5 4.56066L7.43934 6.5L4.96967 8.96967L6.03033 10.0303L8.5 7.56066L10.4697 9.53033L11.5303 8.46967L9.03033 5.96967L6.56066 3.5L7.82323 2.23744Z",
"chevron-left": "M6.23744 8.17678C6.13981 8.07914 6.13981 7.92085 6.23744 7.82322L10.5303 3.53033L9.46967 2.46967L5.17678 6.76256C4.49336 7.44598 4.49336 8.55402 5.17678 9.23744L9.46967 13.5303L10.5303 12.4697L6.23744 8.17678Z", "chevron-left": "M6.23744 8.17678C6.13981 8.07914 6.13981 7.92085 6.23744 7.82322L10.5303 3.53033L9.46967 2.46967L5.17678 6.76256C4.49336 7.44598 4.49336 8.55402 5.17678 9.23744L9.46967 13.5303L10.5303 12.4697L6.23744 8.17678Z",
"chevron-right": "M9.76256 8.17678C9.86019 8.07914 9.86019 7.92085 9.76256 7.82322L5.46967 3.53033L6.53033 2.46967L10.8232 6.76256C11.5066 7.44598 11.5066 8.55402 10.8232 9.23744L6.53033 13.5303L5.46967 12.4697L9.76256 8.17678Z", "chevron-right": "M9.76256 8.17678C9.86019 8.07914 9.86019 7.92085 9.76256 7.82322L5.46967 3.53033L6.53033 2.46967L10.8232 6.76256C11.5066 7.44598 11.5066 8.55402 10.8232 9.23744L6.53033 13.5303L5.46967 12.4697L9.76256 8.17678Z",
"github": "M8.00579 1C4.13177 1 1 4.20832 1 8.17745C1 11.3502 3.00663 14.0358 5.79036 14.9864C6.1384 15.0578 6.26589 14.8319 6.26589 14.6419C6.26589 14.4755 6.25442 13.9052 6.25442 13.3109C4.30557 13.7388 3.89974 12.4553 3.89974 12.4553C3.58655 11.6235 3.1225 11.4097 3.1225 11.4097C2.48465 10.97 3.16896 10.97 3.16896 10.97C3.87651 11.0175 4.24778 11.7067 4.24778 11.7067C4.87402 12.7999 5.88315 12.491 6.28912 12.3009C6.34705 11.8374 6.53276 11.5166 6.72994 11.3384C5.1756 11.172 3.54023 10.5541 3.54023 7.79712C3.54023 7.01283 3.81844 6.37117 4.25926 5.87213C4.1897 5.69392 3.94606 4.95703 4.32895 3.97076C4.32895 3.97076 4.92048 3.78059 6.25427 4.70751C6.82531 4.55039 7.41422 4.47047 8.00579 4.4698C8.59733 4.4698 9.20034 4.55307 9.75717 4.70751C11.0911 3.78059 11.6826 3.97076 11.6826 3.97076C12.0655 4.95703 11.8217 5.69392 11.7522 5.87213C12.2046 6.37117 12.4714 7.01283 12.4714 7.79712C12.4714 10.5541 10.836 11.16 9.27003 11.3384C9.52529 11.5641 9.74555 11.9919 9.74555 12.6692C9.74555 13.6317 9.73408 14.4042 9.73408 14.6418C9.73408 14.8319 9.86171 15.0578 10.2096 14.9865C12.9933 14.0357 15 11.3502 15 8.17745C15.0114 4.20832 11.8682 1 8.00579 1Z" "github": "M8.00579 1C4.13177 1 1 4.20832 1 8.17745C1 11.3502 3.00663 14.0358 5.79036 14.9864C6.1384 15.0578 6.26589 14.8319 6.26589 14.6419C6.26589 14.4755 6.25442 13.9052 6.25442 13.3109C4.30557 13.7388 3.89974 12.4553 3.89974 12.4553C3.58655 11.6235 3.1225 11.4097 3.1225 11.4097C2.48465 10.97 3.16896 10.97 3.16896 10.97C3.87651 11.0175 4.24778 11.7067 4.24778 11.7067C4.87402 12.7999 5.88315 12.491 6.28912 12.3009C6.34705 11.8374 6.53276 11.5166 6.72994 11.3384C5.1756 11.172 3.54023 10.5541 3.54023 7.79712C3.54023 7.01283 3.81844 6.37117 4.25926 5.87213C4.1897 5.69392 3.94606 4.95703 4.32895 3.97076C4.32895 3.97076 4.92048 3.78059 6.25427 4.70751C6.82531 4.55039 7.41422 4.47047 8.00579 4.4698C8.59733 4.4698 9.20034 4.55307 9.75717 4.70751C11.0911 3.78059 11.6826 3.97076 11.6826 3.97076C12.0655 4.95703 11.8217 5.69392 11.7522 5.87213C12.2046 6.37117 12.4714 7.01283 12.4714 7.79712C12.4714 10.5541 10.836 11.16 9.27003 11.3384C9.52529 11.5641 9.74555 11.9919 9.74555 12.6692C9.74555 13.6317 9.73408 14.4042 9.73408 14.6418C9.73408 14.8319 9.86171 15.0578 10.2096 14.9865C12.9933 14.0357 15 11.3502 15 8.17745C15.0114 4.20832 11.8682 1 8.00579 1Z",
"eye-shown": "M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z M7.99998 3.25C4.94324 3.25 2.37688 5.33566 1.31608 7.69213L1.17749 8L1.31608 8.30786C2.37687 10.6643 4.94324 12.75 7.99998 12.75C11.0567 12.75 13.6231 10.6643 14.6839 8.30787L14.8225 8.00001L14.6839 7.69214C13.6231 5.33566 11.0567 3.25 7.99998 3.25ZM7.99998 11.25C5.75461 11.25 3.76954 9.7742 2.83424 8C3.76954 6.2258 5.75462 4.75 7.99998 4.75C10.2453 4.75 12.2304 6.2258 13.1657 8C12.2304 9.7742 10.2453 11.25 7.99998 11.25Z",
"eye-hidden": "M10.3292 1.66459L4.32919 13.6646L5.67083 14.3354L6.54358 12.5899C7.01299 12.6938 7.50015 12.75 8.00001 12.75C11.0567 12.75 13.6231 10.6643 14.6839 8.30787L14.8225 8L14.6839 7.69213C13.9768 6.12144 12.6133 4.68765 10.8911 3.89486L11.6708 2.33541L10.3292 1.66459ZM10.2198 5.23744L7.24143 11.1942C7.49004 11.2309 7.74323 11.25 8.00001 11.25C10.2453 11.25 12.2304 9.77424 13.1657 8.00008C12.5601 6.85225 11.5063 5.817 10.2198 5.23744Z M7.1258 4.82412C5.24551 5.14401 3.6433 6.46644 2.83436 7.99994C3.17611 8.64745 3.66354 9.26476 4.26171 9.78496L3.27737 10.9168C2.42955 10.1795 1.75091 9.27374 1.31611 8.30786L1.17752 7.99999L1.31611 7.69213C2.24746 5.62322 4.32664 3.77878 6.87422 3.34537L7.1258 4.82412Z"
} }

View File

@ -36,7 +36,7 @@
bottom: var(--space-20); bottom: var(--space-20);
right: var(--space-20); right: var(--space-20);
gap: var(--space-8); gap: var(--space-8);
max-width: 22rem; max-width: 30rem;
z-index: 50; z-index: 50;
} }
</style> </style>

View File

@ -2,22 +2,26 @@ import { writable, type Writable } from 'svelte/store';
export type ToastStyle = 'neutral' | 'error' | 'pop' | 'warn'; export type ToastStyle = 'neutral' | 'error' | 'pop' | 'warn';
export interface ToastMessage { export interface Toast {
id?: number; id?: string;
message: string; message: string;
title?: string; title?: string;
style?: ToastStyle; style?: ToastStyle;
} }
export const toastStore: Writable<ToastMessage[]> = writable([]); export const toastStore: Writable<Toast[]> = writable([]);
let idCounter = 0; let idCounter = 0;
export function showToast(message: ToastMessage) { export function showToast(toast: Toast) {
message.message = message.message.replace(/^ */gm, ''); toast.message = toast.message.replace(/^ */gm, '');
toastStore.update((items) => [...items, { id: idCounter++, ...message }]); toastStore.update((items) => [
...items.filter((t) => toast.id == undefined || t.id != toast.id),
{ id: (idCounter++).toString(), ...toast }
]);
} }
export function dismissToast(messageId: number | undefined) { export function dismissToast(messageId: string | undefined) {
if (!messageId) return;
toastStore.update((items) => items.filter((m) => m.id != messageId)); toastStore.update((items) => items.filter((m) => m.id != messageId));
} }

View File

@ -0,0 +1 @@
export const pxToRem = (px: number, base: number = 16) => `${px / base}rem`;

View File

@ -1,4 +1,5 @@
import { invoke } from '$lib/backend/ipc'; import { invoke } from '$lib/backend/ipc';
import { showToast } from '$lib/notifications/toasts';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import posthog from 'posthog-js'; import posthog from 'posthog-js';
import type { RemoteBranchService } from '$lib/stores/remoteBranches'; import type { RemoteBranchService } from '$lib/stores/remoteBranches';
@ -61,7 +62,7 @@ export class BranchController {
}); });
posthog.capture('Commit Successful'); posthog.capture('Commit Successful');
} catch (err: any) { } catch (err: any) {
toasts.error('Failed to commit branch'); toasts.error('Failed to commit changes');
posthog.capture('Commit Failed', err); posthog.capture('Commit Failed', err);
} }
} }
@ -193,10 +194,33 @@ export class BranchController {
await this.vbranchService.reload(); await this.vbranchService.reload();
return await this.vbranchService.getById(branchId); return await this.vbranchService.getById(branchId);
} catch (err: any) { } catch (err: any) {
console.error(err);
if (err.code === 'errors.git.authentication') { if (err.code === 'errors.git.authentication') {
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?'); showToast({
title: 'Git push failed',
message: `
Your branch cannot be pushed due to an authentication failure.
Please check our [documentation](https://docs.gitbutler.com/troubleshooting/fetch-push)
on fetching and pushing for ways to resolve the problem.
\`\`\`${err.message}\`\`\`
`,
style: 'error'
});
} else { } else {
toasts.error(`Failed to push branch: ${err.message}`); showToast({
title: 'Git push failed',
message: `
Your branch cannot be pushed due to an unforeseen problem.
Please check our [documentation](https://docs.gitbutler.com/troubleshooting/fetch-push)
on fetching and pushing for ways to resolve the problem.
\`\`\`${err.message}\`\`\`
`,
style: 'error'
});
} }
} }
} }

View File

@ -1,8 +1,7 @@
import { BaseBranch, Branch } from './types'; import { BaseBranch, Branch } from './types';
import { invoke, listen } from '$lib/backend/ipc'; import { Code, invoke, listen } from '$lib/backend/ipc';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import posthog from 'posthog-js';
import { import {
switchMap, switchMap,
Observable, Observable,
@ -101,26 +100,6 @@ export class VirtualBranchService {
) )
); );
} }
async pushBranch(branchId: string, withForce: boolean): Promise<Branch | undefined> {
try {
await invoke<void>('push_virtual_branch', {
projectId: this.projectId,
branchId,
withForce
});
posthog.capture('Push Successful');
await this.reload();
return await this.getById(branchId);
} catch (err: any) {
posthog.capture('Push Failed', { error: err });
if (err.code === 'errors.git.authentication') {
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?');
} else {
toasts.error(`Failed to push branch: ${err.message}`);
}
}
}
} }
function subscribeToVirtualBranches(projectId: string, callback: (branches: Branch[]) => void) { function subscribeToVirtualBranches(projectId: string, callback: (branches: Branch[]) => void) {
@ -147,6 +126,9 @@ export class BaseBranchService {
return await getBaseBranch({ projectId }); return await getBaseBranch({ projectId });
}), }),
tap(() => this.busy$.next(false)), tap(() => this.busy$.next(false)),
// Start with undefined to prevent delay in updating $baseBranch$ value in
// layout.svelte when navigating between projects.
startWith(undefined),
shareReplay(1), shareReplay(1),
catchError((e) => { catchError((e) => {
this.error$.next(e); this.error$.next(e);
@ -162,11 +144,15 @@ export class BaseBranchService {
// trigger a base branch reload. It feels a bit awkward and should be improved. // trigger a base branch reload. It feels a bit awkward and should be improved.
await invoke<void>('fetch_from_target', { projectId: this.projectId }); await invoke<void>('fetch_from_target', { projectId: this.projectId });
} catch (err: any) { } catch (err: any) {
if (err.code === 'errors.git.authentication') { if (err.message?.includes('does not have a default target')) {
// Swallow this error since user should be taken to project setup page
return;
} else if (err.code === Code.ProjectsGitAuth) {
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?'); toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?');
} else { } else {
toasts.error(`Failed to fetch branch: ${err.message}`); toasts.error(`Failed to fetch branch: ${err.message}`);
} }
console.error(err);
} }
} }

View File

@ -290,6 +290,13 @@ export class BaseBranch {
} }
commitUrl(commitId: string): string | undefined { commitUrl(commitId: string): string | undefined {
// if repoBaseUrl is bitbucket, then the commit url is different
if (this.repoBaseUrl.includes('bitbucket.org')) {
return `${this.repoBaseUrl}/commits/${commitId}`;
}
if (this.repoBaseUrl.includes('gitlab.com')) {
return `${this.repoBaseUrl}/-/commit/${commitId}`;
}
return `${this.repoBaseUrl}/commit/${commitId}`; return `${this.repoBaseUrl}/commit/${commitId}`;
} }

View File

@ -2,6 +2,7 @@
import '../styles/main.postcss'; import '../styles/main.postcss';
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte'; import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
import UpdateButton from '$lib/components/UpdateButton.svelte';
import ToastController from '$lib/notifications/ToastController.svelte'; import ToastController from '$lib/notifications/ToastController.svelte';
import { SETTINGS_CONTEXT, loadUserSettings } from '$lib/settings/userSettings'; import { SETTINGS_CONTEXT, loadUserSettings } from '$lib/settings/userSettings';
import * as events from '$lib/utils/events'; import * as events from '$lib/utils/events';
@ -14,7 +15,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
export let data: LayoutData; export let data: LayoutData;
const { cloud, user$ } = data; const { cloud, user$, updaterService } = data;
const userSettings = loadUserSettings(); const userSettings = loadUserSettings();
initTheme(userSettings); initTheme(userSettings);
@ -51,3 +52,4 @@
<Toaster /> <Toaster />
<ShareIssueModal bind:this={shareIssueModal} user={$user$} {cloud} /> <ShareIssueModal bind:this={shareIssueModal} user={$user$} {cloud} />
<ToastController /> <ToastController />
<UpdateButton {updaterService} />

View File

@ -2,6 +2,7 @@ import { initPostHog } from '$lib/analytics/posthog';
import { initSentry } from '$lib/analytics/sentry'; import { initSentry } from '$lib/analytics/sentry';
import { getCloudApiClient } from '$lib/backend/cloud'; import { getCloudApiClient } from '$lib/backend/cloud';
import { ProjectService } from '$lib/backend/projects'; import { ProjectService } from '$lib/backend/projects';
import { UpdaterService } from '$lib/backend/updater';
import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings'; import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings';
import { UserService } from '$lib/stores/user'; import { UserService } from '$lib/stores/user';
import lscache from 'lscache'; import lscache from 'lscache';
@ -32,6 +33,7 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
if (enabled) initPostHog(); if (enabled) initPostHog();
}); });
const userService = new UserService(); const userService = new UserService();
const updaterService = new UpdaterService();
// TODO: Find a workaround to avoid this dynamic import // TODO: Find a workaround to avoid this dynamic import
// https://github.com/sveltejs/kit/issues/905 // https://github.com/sveltejs/kit/issues/905
@ -41,6 +43,7 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
return { return {
projectService: new ProjectService(defaultPath), projectService: new ProjectService(defaultPath),
cloud: getCloudApiClient({ fetch: realFetch }), cloud: getCloudApiClient({ fetch: realFetch }),
updaterService,
userService, userService,
user$: userService.user$ user$: userService.user$
}; };

View File

@ -9,11 +9,8 @@
import * as hotkeys from '$lib/utils/hotkeys'; import * as hotkeys from '$lib/utils/hotkeys';
import { unsubscribe } from '$lib/utils/random'; import { unsubscribe } from '$lib/utils/random';
import { getRemoteBranches } from '$lib/vbranches/branchStoresCache'; import { getRemoteBranches } from '$lib/vbranches/branchStoresCache';
import { interval, Subscription } from 'rxjs'; import { onDestroy, onMount } from 'svelte';
import { startWith, tap } from 'rxjs/operators';
import { onMount } from 'svelte';
import type { LayoutData } from './$types'; import type { LayoutData } from './$types';
import { page } from '$app/stores';
export let data: LayoutData; export let data: LayoutData;
@ -35,28 +32,31 @@
$: user$ = data.user$; $: user$ = data.user$;
let trayViewport: HTMLElement; let trayViewport: HTMLElement;
let intervalId: any;
handleMenuActions(data.projectId); handleMenuActions(data.projectId);
let lastProjectId: string | undefined = undefined; // Once on load and every time the project id changes
onMount(() => { $: if (projectId) setupFetchInterval();
let fetchSub: Subscription;
// Project is auto-fetched on page load and then every 15 minutes function setupFetchInterval() {
page.subscribe((page) => { baseBranchService.fetchFromTarget();
if (page.params.projectId !== lastProjectId) { clearFetchInterval();
lastProjectId = page.params.projectId; intervalId = setInterval(() => baseBranchService.fetchFromTarget(), 15 * 60 * 1000);
fetchSub?.unsubscribe();
fetchSub = interval(1000 * 60 * 15)
.pipe(
startWith(0),
tap(() => baseBranchService.fetchFromTarget())
)
.subscribe();
} }
});
return unsubscribe( function clearFetchInterval() {
if (intervalId) clearInterval(intervalId);
}
onMount(() =>
unsubscribe(
menuSubscribe(data.projectId), menuSubscribe(data.projectId),
hotkeys.on('Meta+Shift+S', () => syncToCloud($project$?.id)) hotkeys.on('Meta+Shift+S', () => syncToCloud(projectId))
)
); );
onDestroy(() => {
clearFetchInterval();
}); });
</script> </script>
@ -65,8 +65,7 @@
{:else if $baseError$} {:else if $baseError$}
<ProblemLoadingRepo {projectService} {userService} project={$project$} error={$baseError$} /> <ProblemLoadingRepo {projectService} {userService} project={$project$} error={$baseError$} />
{:else if $baseBranch$ === null} {:else if $baseBranch$ === null}
{@const remoteBranches = getRemoteBranches(projectId)} {#await getRemoteBranches(projectId)}
{#await remoteBranches}
<p>loading...</p> <p>loading...</p>
{:then remoteBranches} {:then remoteBranches}
{#if remoteBranches.length == 0} {#if remoteBranches.length == 0}

View File

@ -34,7 +34,12 @@ export const load: LayoutLoad = async ({ params, parent }) => {
); );
const githubService = new GitHubService(userService, baseBranchService); const githubService = new GitHubService(userService, baseBranchService);
const branchService = new BranchService(vbranchService, remoteBranchService, githubService); const branchService = new BranchService(
vbranchService,
remoteBranchService,
githubService,
branchController
);
return { return {
projectId, projectId,

View File

@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import CloudForm from '$lib/components/CloudForm.svelte'; import CloudForm from '$lib/components/CloudForm.svelte';
import DetailsForm from '$lib/components/DetailsForm.svelte'; import DetailsForm from '$lib/components/DetailsForm.svelte';
import FullscreenLoading from '$lib/components/FullscreenLoading.svelte';
import KeysForm from '$lib/components/KeysForm.svelte'; import KeysForm from '$lib/components/KeysForm.svelte';
import PreferencesForm from '$lib/components/PreferencesForm.svelte'; import PreferencesForm from '$lib/components/PreferencesForm.svelte';
import RemoveProjectButton from '$lib/components/RemoveProjectButton.svelte'; import RemoveProjectButton from '$lib/components/RemoveProjectButton.svelte';
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte'; import SectionCard from '$lib/components/SectionCard.svelte';
import Spacer from '$lib/components/Spacer.svelte'; import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import type { UserError } from '$lib/backend/ipc'; import type { UserError } from '$lib/backend/ipc';
import type { Key, Project } from '$lib/backend/projects'; import type { Key, Project } from '$lib/backend/projects';
@ -66,50 +67,21 @@
}; };
</script> </script>
<ScrollableContainer wide> {#if !$project$}
<div class="settings" data-tauri-drag-region> <FullscreenLoading />
<div class="card"> {:else}
{#if !$project$} <ContentWrapper title="Project settings">
loading...
{:else}
<div class="card__header">
<span class="card_title text-base-16 text-semibold">Project settings</span>
</div>
<div class="card__content">
<CloudForm project={$project$} user={$user$} {userService} on:updated={onCloudUpdated} /> <CloudForm project={$project$} user={$user$} {userService} on:updated={onCloudUpdated} />
<Spacer margin={2} />
<DetailsForm project={$project$} on:updated={onDetailsUpdated} /> <DetailsForm project={$project$} on:updated={onDetailsUpdated} />
<Spacer margin={2} />
<KeysForm project={$project$} on:updated={onKeysUpdated} /> <KeysForm project={$project$} on:updated={onKeysUpdated} />
<Spacer margin={2} />
<PreferencesForm project={$project$} on:updated={onPreferencesUpdated} /> <PreferencesForm project={$project$} on:updated={onPreferencesUpdated} />
<Spacer margin={2} /> <SectionCard>
<svelte:fragment slot="title">Remove project</svelte:fragment>
<div class="flex gap-x-4"> <svelte:fragment slot="body">
<a You can remove projects from GitButler, your code remains safe as this only clears
href="https://discord.gg/wDKZCPEjXC" configuration.
target="_blank" </svelte:fragment>
rel="noreferrer" <div>
class="flex-1 rounded border border-light-200 bg-white p-4 dark:border-dark-400 dark:bg-dark-700"
>
<p class="mb-2 font-medium">Join our Discord</p>
<p class="text-light-700 dark:text-dark-200">
Join our community and share feedback, requests, or ask a question.
</p>
</a>
<a
href="mailto:hello@gitbutler.com?subject=Feedback or question!"
target="_blank"
class="flex-1 rounded border border-light-200 bg-white p-4 dark:border-dark-400 dark:bg-dark-700"
>
<p class="mb-2 font-medium">Contact us</p>
<p class="text-light-700 dark:text-dark-200">
If you have an issue or any questions, contact us.
</p>
</a>
</div>
</div>
<div class="card__footer">
<RemoveProjectButton <RemoveProjectButton
bind:this={deleteConfirmationModal} bind:this={deleteConfirmationModal}
projectTitle={$project$?.title} projectTitle={$project$?.title}
@ -117,23 +89,6 @@
{onDeleteClicked} {onDeleteClicked}
/> />
</div> </div>
{/if} </SectionCard>
</div> </ContentWrapper>
</div> {/if}
</ScrollableContainer>
<style lang="postcss">
.settings {
display: flex;
flex-direction: column;
padding: var(--space-16) var(--space-16);
height: 100%;
width: 100%;
}
.card {
max-width: 50rem;
}
.card__content {
gap: var(--space-24);
}
</style>

View File

@ -71,14 +71,6 @@ button {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
div,
button,
[role='button'],
select,
a {
cursor: default;
}
/* SCROLL BAR STYLING */ /* SCROLL BAR STYLING */
/* We don't use REM here becasue we don't want /* We don't use REM here becasue we don't want
the scrollbar to scale with the font size */ the scrollbar to scale with the font size */