mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 02:26:14 +03:00
merge upstream
This commit is contained in:
commit
f147f12db0
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -5,7 +5,6 @@
|
||||
"rust-lang.rust-analyzer",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ZixuanChen.vitest-explorer",
|
||||
"EditorConfig.EditorConfig"
|
||||
]
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ impl Default for Options {
|
||||
pub fn workdir(
|
||||
repository: &Repository,
|
||||
commit_oid: &git::Oid,
|
||||
context_lines: u32,
|
||||
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
|
||||
let commit = repository
|
||||
.find_commit(*commit_oid)
|
||||
@ -72,7 +73,7 @@ pub fn workdir(
|
||||
.show_binary(true)
|
||||
.show_untracked_content(true)
|
||||
.ignore_submodules(true)
|
||||
.context_lines(0);
|
||||
.context_lines(context_lines);
|
||||
|
||||
let diff = repository.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?;
|
||||
|
||||
@ -83,6 +84,7 @@ pub fn trees(
|
||||
repository: &Repository,
|
||||
old_tree: &git::Tree,
|
||||
new_tree: &git::Tree,
|
||||
context_lines: u32,
|
||||
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
|
||||
let mut diff_opts = git2::DiffOptions::new();
|
||||
diff_opts
|
||||
@ -90,7 +92,7 @@ pub fn trees(
|
||||
.include_untracked(true)
|
||||
.show_binary(true)
|
||||
.ignore_submodules(true)
|
||||
.context_lines(0)
|
||||
.context_lines(context_lines)
|
||||
.show_untracked_content(true);
|
||||
|
||||
let diff =
|
||||
@ -340,7 +342,7 @@ mod tests {
|
||||
|
||||
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[&path::PathBuf::from("file")],
|
||||
@ -363,7 +365,7 @@ mod tests {
|
||||
|
||||
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[&path::PathBuf::from("first")],
|
||||
@ -387,7 +389,7 @@ mod tests {
|
||||
|
||||
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[&path::PathBuf::from("first")],
|
||||
@ -431,7 +433,7 @@ mod tests {
|
||||
|
||||
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[&path::PathBuf::from("image")],
|
||||
vec![Hunk {
|
||||
@ -474,7 +476,7 @@ mod tests {
|
||||
|
||||
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[&path::PathBuf::from("file")],
|
||||
vec![Hunk {
|
||||
|
@ -365,6 +365,13 @@ impl Repository {
|
||||
if self.project.omit_certificate_check.unwrap_or(false) {
|
||||
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(
|
||||
&[refspec.as_str()],
|
||||
Some(&mut git2::PushOptions::new().remote_callbacks(cbs)),
|
||||
|
@ -70,6 +70,10 @@ impl From<controller::AddError> for Error {
|
||||
code: Code::Projects,
|
||||
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::Other(error) => {
|
||||
tracing::error!(?error, "failed to add project");
|
||||
|
@ -66,6 +66,10 @@ impl Controller {
|
||||
return Err(AddError::NotAGitRepository);
|
||||
};
|
||||
|
||||
if path.join(".gitmodules").exists() {
|
||||
return Err(AddError::SubmodulesNotSupported);
|
||||
}
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
// title is the base name of the file
|
||||
@ -79,6 +83,7 @@ impl Controller {
|
||||
title,
|
||||
path: path.to_path_buf(),
|
||||
api: None,
|
||||
use_diff_context: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@ -201,6 +206,10 @@ impl Controller {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@ -253,6 +262,8 @@ pub enum AddError {
|
||||
PathNotFound,
|
||||
#[error("project already exists")]
|
||||
AlreadyExists,
|
||||
#[error("submodules not supported")]
|
||||
SubmodulesNotSupported,
|
||||
#[error(transparent)]
|
||||
User(#[from] users::GetError),
|
||||
#[error(transparent)]
|
||||
|
@ -78,6 +78,8 @@ pub struct Project {
|
||||
pub project_data_last_fetch: Option<FetchResult>,
|
||||
#[serde(default)]
|
||||
pub omit_certificate_check: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub use_diff_context: Option<bool>,
|
||||
}
|
||||
|
||||
impl AsRef<Project> for Project {
|
||||
|
@ -47,6 +47,7 @@ pub struct UpdateRequest {
|
||||
pub gitbutler_code_push_state: Option<project::CodePushState>,
|
||||
pub project_data_last_fetched: Option<project::FetchResult>,
|
||||
pub omit_certificate_check: Option<bool>,
|
||||
pub use_diff_context: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@ -139,6 +140,10 @@ impl Storage {
|
||||
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
|
||||
.write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?;
|
||||
|
||||
|
@ -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
|
||||
// put them into a virtual branch
|
||||
|
||||
let wd_diff = diff::workdir(repo, ¤t_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, ¤t_head_commit.id(), context_lines)?;
|
||||
if !wd_diff.is_empty() || current_head_commit.id() != target.sha {
|
||||
let hunks_by_filepath =
|
||||
super::virtual_hunks_by_filepath(&project_repository.project().path, &wd_diff);
|
||||
@ -308,6 +313,12 @@ pub fn update_base_branch(
|
||||
let 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
|
||||
let updated_vbranches = super::get_status_by_branch(gb_repository, project_repository)?
|
||||
.into_iter()
|
||||
@ -341,6 +352,7 @@ pub fn update_base_branch(
|
||||
&project_repository.git_repository,
|
||||
&branch_head_tree,
|
||||
&branch_tree,
|
||||
context_lines,
|
||||
)?;
|
||||
if non_commited_files.is_empty() {
|
||||
// if there are no commited files, then the branch is fully merged
|
||||
|
@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
branch::BranchId,
|
||||
branch::{self, BranchId},
|
||||
controller::{Controller, ControllerError},
|
||||
BaseBranch, RemoteBranchFile,
|
||||
};
|
||||
@ -80,11 +80,23 @@ pub async fn list_virtual_branches(
|
||||
code: Code::Validation,
|
||||
message: "Malformed project id".to_string(),
|
||||
})?;
|
||||
let branches = handle
|
||||
let (branches, uses_diff_context) = handle
|
||||
.state::<Controller>()
|
||||
.list_virtual_branches(&project_id)
|
||||
.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 branches = proxy.proxy_virtual_branches(branches).await;
|
||||
Ok(branches)
|
||||
|
@ -124,7 +124,8 @@ impl Controller {
|
||||
pub async fn list_virtual_branches(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
) -> Result<Vec<super::VirtualBranch>, ControllerError<errors::ListVirtualBranchesError>> {
|
||||
) -> Result<(Vec<super::VirtualBranch>, bool), ControllerError<errors::ListVirtualBranchesError>>
|
||||
{
|
||||
self.inner(project_id)
|
||||
.await
|
||||
.list_virtual_branches(project_id)
|
||||
@ -493,7 +494,8 @@ impl ControllerInner {
|
||||
pub async fn list_virtual_branches(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
) -> Result<Vec<super::VirtualBranch>, ControllerError<errors::ListVirtualBranchesError>> {
|
||||
) -> Result<(Vec<super::VirtualBranch>, bool), ControllerError<errors::ListVirtualBranchesError>>
|
||||
{
|
||||
let _permit = self.semaphore.acquire().await;
|
||||
|
||||
self.with_verify_branch(project_id, |gb_repository, project_repository, _| {
|
||||
@ -570,9 +572,17 @@ impl ControllerInner {
|
||||
) -> Result<Vec<RemoteBranchFile>, Error> {
|
||||
let project = self.projects.get(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)?;
|
||||
|
||||
super::list_remote_commit_files(&project_repository.git_repository, commit_oid)
|
||||
.map_err(Into::into)
|
||||
let use_context = project_repository
|
||||
.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)
|
||||
}
|
||||
|
||||
pub fn set_base_branch(
|
||||
|
@ -19,6 +19,7 @@ pub struct RemoteBranchFile {
|
||||
pub fn list_remote_commit_files(
|
||||
repository: &git::Repository,
|
||||
commit_oid: git::Oid,
|
||||
context_lines: u32,
|
||||
) -> Result<Vec<RemoteBranchFile>, errors::ListRemoteCommitFilesError> {
|
||||
let commit = match repository.find_commit(commit_oid) {
|
||||
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 commit_tree = commit.tree().context("failed to get commit 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
|
||||
.into_iter()
|
||||
|
@ -81,7 +81,7 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
|
||||
"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];
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
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)
|
||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let branch = &branches[0];
|
||||
assert_eq!(branch.files.len(), 0);
|
||||
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
|
||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let branch = &branches[0];
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
assert_eq!(branch.commits.len(), 1);
|
||||
@ -170,7 +170,7 @@ fn test_signed_commit() -> Result<()> {
|
||||
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_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
||||
// 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"))?;
|
||||
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];
|
||||
assert_eq!(branch.files.len(), 2);
|
||||
let img_file = &branch
|
||||
@ -262,7 +262,7 @@ fn test_track_binary_files() -> Result<()> {
|
||||
)?;
|
||||
|
||||
// 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_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
||||
let tree = commit_obj.tree()?;
|
||||
@ -291,7 +291,7 @@ fn test_track_binary_files() -> Result<()> {
|
||||
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;
|
||||
// get tree from 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");
|
||||
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!(
|
||||
files[0].hunks[0].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;
|
||||
assert_eq!(
|
||||
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!(
|
||||
@ -667,7 +667,7 @@ fn test_updated_ownership_should_bubble_up() -> Result<()> {
|
||||
let files2 = branch_reader.read(&branch1_id)?.ownership.files;
|
||||
assert_eq!(
|
||||
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!(
|
||||
@ -736,12 +736,12 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
|
||||
let branch_writer = branch::Writer::new(&gb_repository)?;
|
||||
let mut branch2 = branch_reader.read(&branch2_id)?;
|
||||
branch2.ownership = Ownership {
|
||||
files: vec!["test.txt:1-2".parse()?],
|
||||
files: vec!["test.txt:1-5".parse()?],
|
||||
};
|
||||
branch_writer.write(&mut branch2)?;
|
||||
let mut branch1 = branch_reader.read(&branch1_id)?;
|
||||
branch1.ownership = Ownership {
|
||||
files: vec!["test.txt:14-15".parse()?],
|
||||
files: vec!["test.txt:11-15".parse()?],
|
||||
};
|
||||
branch_writer.write(&mut branch1)?;
|
||||
|
||||
@ -765,7 +765,7 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
|
||||
&project_repository,
|
||||
branch::BranchUpdateRequest {
|
||||
id: branch3_id,
|
||||
ownership: Some("test.txt:1-2,14-15".parse()?),
|
||||
ownership: Some("test.txt:1-5,11-15".parse()?),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
@ -788,11 +788,11 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
|
||||
);
|
||||
assert_eq!(
|
||||
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!(
|
||||
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(())
|
||||
}
|
||||
@ -848,7 +848,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
|
||||
&project_repository,
|
||||
branch::BranchUpdateRequest {
|
||||
id: branch2_id,
|
||||
ownership: Some("test.txt:1-2".parse()?),
|
||||
ownership: Some("test.txt:1-5".parse()?),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
@ -869,7 +869,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
|
||||
);
|
||||
assert_eq!(
|
||||
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);
|
||||
@ -879,7 +879,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
|
||||
);
|
||||
assert_eq!(
|
||||
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(())
|
||||
@ -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");
|
||||
assert_eq!(
|
||||
statuses[0].1[std::path::Path::new("test.txt")][0].diff,
|
||||
"@@ -14 +13,0 @@ line13\n-line13\n"
|
||||
);
|
||||
assert_eq!(
|
||||
statuses[0].1[std::path::Path::new("test.txt")][1].diff,
|
||||
"@@ -15,0 +15 @@ line14\n+line15\n"
|
||||
"@@ -11,5 +11,5 @@ line10\n line11\n line12\n line13\n-line13\n line14\n+line15\n"
|
||||
);
|
||||
|
||||
std::fs::write(
|
||||
@ -932,15 +928,11 @@ fn test_add_new_hunk_to_the_end() -> Result<()> {
|
||||
|
||||
assert_eq!(
|
||||
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!(
|
||||
statuses[0].1[std::path::Path::new("test.txt")][1].diff,
|
||||
"@@ -0,0 +1 @@\n+line0\n"
|
||||
);
|
||||
assert_eq!(
|
||||
statuses[0].1[std::path::Path::new("test.txt")][2].diff,
|
||||
"@@ -14 +14,0 @@ line13\n-line13\n"
|
||||
"@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@ -1040,7 +1032,7 @@ fn test_merge_vbranch_upstream_clean_rebase() -> Result<()> {
|
||||
.context("failed to write target branch after push")?;
|
||||
|
||||
// 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];
|
||||
assert_eq!(branch1.files.len(), 1);
|
||||
assert_eq!(branch1.commits.len(), 1);
|
||||
@ -1054,7 +1046,7 @@ fn test_merge_vbranch_upstream_clean_rebase() -> Result<()> {
|
||||
None,
|
||||
)?;
|
||||
|
||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let branch1 = &branches[0];
|
||||
|
||||
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")?;
|
||||
|
||||
// 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];
|
||||
|
||||
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)?;
|
||||
|
||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let branch1 = &branches[0];
|
||||
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
|
||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
assert!(branches[0].conflicted);
|
||||
|
||||
// commit the merge resolution
|
||||
@ -1207,7 +1199,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
|
||||
false,
|
||||
)?;
|
||||
|
||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||
let branch1 = &branches[0];
|
||||
assert!(!branch1.conflicted);
|
||||
assert_eq!(branch1.files.len(), 0);
|
||||
@ -1247,7 +1239,7 @@ fn test_unapply_ownership_partial() -> Result<()> {
|
||||
)
|
||||
.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[0].files.len(), 1);
|
||||
assert_eq!(branches[0].ownership.files.len(), 1);
|
||||
@ -1261,11 +1253,11 @@ fn test_unapply_ownership_partial() -> Result<()> {
|
||||
unapply_ownership(
|
||||
&gb_repository,
|
||||
&project_repository,
|
||||
&"test.txt:5-6".parse().unwrap(),
|
||||
&"test.txt:2-6".parse().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[0].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))?;
|
||||
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();
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
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))?;
|
||||
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();
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
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))?;
|
||||
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();
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
assert!(branch.active);
|
||||
@ -1621,7 +1613,7 @@ fn test_detect_mergeable_branch() -> Result<()> {
|
||||
};
|
||||
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);
|
||||
|
||||
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||
@ -1803,7 +1795,7 @@ fn test_upstream_integrated_vbranch() -> Result<()> {
|
||||
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();
|
||||
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",
|
||||
)?;
|
||||
|
||||
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();
|
||||
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
@ -1869,7 +1861,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
|
||||
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();
|
||||
|
||||
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",
|
||||
)?;
|
||||
|
||||
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();
|
||||
|
||||
assert_eq!(branch.files.len(), 1, "one file should be changed");
|
||||
@ -1906,7 +1898,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
|
||||
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();
|
||||
|
||||
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",
|
||||
)?;
|
||||
|
||||
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();
|
||||
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
@ -1970,7 +1962,7 @@ fn test_commit_same_file_twice() -> Result<()> {
|
||||
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();
|
||||
|
||||
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",
|
||||
)?;
|
||||
|
||||
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();
|
||||
|
||||
assert_eq!(branch.files.len(), 1, "one file should be changed");
|
||||
@ -2007,7 +1999,7 @@ fn test_commit_same_file_twice() -> Result<()> {
|
||||
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();
|
||||
|
||||
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",
|
||||
)?;
|
||||
|
||||
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();
|
||||
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
@ -2065,13 +2057,13 @@ fn test_commit_partial_by_hunk() -> Result<()> {
|
||||
&project_repository,
|
||||
&branch1_id,
|
||||
"first commit to test.txt",
|
||||
Some(&"test.txt:2-3".parse::<Ownership>().unwrap()),
|
||||
Some(&"test.txt:1-6".parse::<Ownership>().unwrap()),
|
||||
None,
|
||||
None,
|
||||
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();
|
||||
|
||||
assert_eq!(branch.files.len(), 1);
|
||||
@ -2085,13 +2077,13 @@ fn test_commit_partial_by_hunk() -> Result<()> {
|
||||
&project_repository,
|
||||
&branch1_id,
|
||||
"second commit to test.txt",
|
||||
Some(&"test.txt:19-20".parse::<Ownership>().unwrap()),
|
||||
Some(&"test.txt:16-22".parse::<Ownership>().unwrap()),
|
||||
None,
|
||||
None,
|
||||
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();
|
||||
|
||||
assert_eq!(branch.files.len(), 0);
|
||||
@ -2158,7 +2150,7 @@ fn test_commit_partial_by_file() -> Result<()> {
|
||||
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();
|
||||
|
||||
// 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,
|
||||
)?;
|
||||
|
||||
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();
|
||||
|
||||
// branch one test.txt has just the 1st and 3rd hunks applied
|
||||
@ -2305,7 +2297,7 @@ fn test_commit_executable_and_symlinks() -> Result<()> {
|
||||
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 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();
|
||||
|
||||
// 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);
|
||||
|
||||
let branch = &virtual_branches.first().unwrap();
|
||||
|
@ -4,8 +4,9 @@ use std::{collections::HashMap, path, time, vec};
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use diffy::{apply_bytes, Patch};
|
||||
use git2_hooks::{HookResult, PrepareCommitMsgSource};
|
||||
use bstr::ByteSlice;
|
||||
use diffy::{apply, Patch};
|
||||
use git2_hooks::HookResult;
|
||||
use regex::Regex;
|
||||
use serde::Serialize;
|
||||
|
||||
@ -728,7 +729,7 @@ fn find_base_tree<'a>(
|
||||
pub fn list_virtual_branches(
|
||||
gb_repository: &gb_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 default_target = gb_repository
|
||||
@ -889,21 +890,29 @@ pub fn list_virtual_branches(
|
||||
|
||||
let branches = branches_with_large_files_abridged(branches);
|
||||
let mut branches = branches_with_hunk_locks(branches, project_repository)?;
|
||||
for branch in &mut branches {
|
||||
branch.files = files_with_hunk_context(
|
||||
&project_repository.git_repository,
|
||||
branch.files.clone(),
|
||||
3,
|
||||
branch.head,
|
||||
)
|
||||
.context("failed to add hunk context")?;
|
||||
|
||||
// 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 {
|
||||
branch.files = files_with_hunk_context(
|
||||
&project_repository.git_repository,
|
||||
branch.files.clone(),
|
||||
3,
|
||||
branch.head,
|
||||
)
|
||||
.context("failed to add hunk context")?;
|
||||
}
|
||||
}
|
||||
|
||||
branches.sort_by(|a, b| a.order.cmp(&b.order));
|
||||
|
||||
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> {
|
||||
@ -940,6 +949,7 @@ fn branches_with_hunk_locks(
|
||||
&project_repository.git_repository,
|
||||
&parent_tree,
|
||||
&commit_tree,
|
||||
context_lines(project_repository),
|
||||
)?;
|
||||
for branch in &mut branches {
|
||||
for file in &mut branch.files {
|
||||
@ -1091,6 +1101,7 @@ pub fn calculate_non_commited_diffs(
|
||||
&project_repository.git_repository,
|
||||
&branch_head,
|
||||
&branch_tree,
|
||||
context_lines(project_repository),
|
||||
)
|
||||
.context("failed to diff trees")?;
|
||||
|
||||
@ -1132,6 +1143,7 @@ fn list_virtual_commit_files(
|
||||
&project_repository.git_repository,
|
||||
&parent_tree,
|
||||
&commit_tree,
|
||||
context_lines(project_repository),
|
||||
)?;
|
||||
let hunks_by_filepath = virtual_hunks_by_filepath(&project_repository.project().path, &diff);
|
||||
Ok(virtual_hunks_to_virtual_files(
|
||||
@ -1895,6 +1907,7 @@ fn get_non_applied_status(
|
||||
&project_repository.git_repository,
|
||||
&target_tree,
|
||||
&branch_tree,
|
||||
context_lines(project_repository),
|
||||
)?;
|
||||
|
||||
Ok((branch, diff))
|
||||
@ -1913,8 +1926,12 @@ fn get_applied_status(
|
||||
default_target: &target::Target,
|
||||
mut virtual_branches: Vec<branch::Branch>,
|
||||
) -> Result<AppliedStatuses> {
|
||||
let mut diff = diff::workdir(&project_repository.git_repository, &default_target.sha)
|
||||
.context("failed to diff workdir")?;
|
||||
let mut diff = diff::workdir(
|
||||
&project_repository.git_repository,
|
||||
&default_target.sha,
|
||||
context_lines(project_repository),
|
||||
)
|
||||
.context("failed to diff workdir")?;
|
||||
|
||||
// sort by order, so that the default branch is first (left in the ui)
|
||||
virtual_branches.sort_by(|a, b| a.order.cmp(&b.order));
|
||||
@ -2291,39 +2308,37 @@ pub fn write_tree_onto_tree(
|
||||
.peel_to_blob()
|
||||
.context("failed to get blob")?;
|
||||
|
||||
// get the contents
|
||||
let mut blob_contents = blob.content().to_vec();
|
||||
let mut blob_contents = blob.content().to_str()?.to_string();
|
||||
|
||||
let mut hunks = hunks.clone();
|
||||
hunks.sort_by_key(|hunk| hunk.new_start);
|
||||
let mut all_diffs = String::new();
|
||||
for hunk in hunks {
|
||||
let patch = format!("--- original\n+++ modified\n{}", 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))?;
|
||||
all_diffs.push_str(&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
|
||||
let new_blob_oid = git_repository.blob(&blob_contents)?;
|
||||
let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?;
|
||||
// upsert into the builder
|
||||
builder.upsert(rel_path, new_blob_oid, filemode);
|
||||
}
|
||||
} else if is_submodule {
|
||||
let mut blob_contents = vec![];
|
||||
let mut blob_contents = String::new();
|
||||
|
||||
let mut hunks = hunks.clone();
|
||||
hunks.sort_by_key(|hunk| hunk.new_start);
|
||||
for hunk in hunks {
|
||||
let patch = format!("--- original\n+++ modified\n{}", hunk.diff);
|
||||
let patch_bytes = patch.as_bytes();
|
||||
let patch = Patch::from_bytes(patch_bytes)?;
|
||||
blob_contents = apply_bytes(&blob_contents, &patch)
|
||||
let patch = Patch::from_str(&hunk.diff)?;
|
||||
blob_contents = apply(&blob_contents, &patch)
|
||||
.context(format!("failed to apply {}", &hunk.diff))?;
|
||||
}
|
||||
|
||||
// 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
|
||||
builder.upsert(rel_path, new_blob_oid, filemode);
|
||||
} else {
|
||||
@ -3636,6 +3651,7 @@ pub fn move_commit(
|
||||
&project_repository.git_repository,
|
||||
&source_branch_head_parent_tree,
|
||||
&source_branch_head_tree,
|
||||
context_lines(project_repository),
|
||||
)?;
|
||||
|
||||
let is_source_locked = source_branch_non_comitted_files
|
||||
@ -3836,6 +3852,7 @@ pub fn create_virtual_branch_from_branch(
|
||||
&project_repository.git_repository,
|
||||
&merge_base_tree,
|
||||
&head_commit_tree,
|
||||
context_lines(project_repository),
|
||||
)
|
||||
.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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -79,7 +79,7 @@ impl InnerHandler {
|
||||
.list_virtual_branches(project_id)
|
||||
.await
|
||||
{
|
||||
Ok(branches) => Ok(vec![events::Event::Emit(
|
||||
Ok((branches, _)) => Ok(vec![events::Event::Emit(
|
||||
app_events::Event::virtual_branches(
|
||||
project_id,
|
||||
&self.assets_proxy.proxy_virtual_branches(branches).await,
|
||||
|
@ -27,7 +27,7 @@
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": true,
|
||||
"dialog": false,
|
||||
"endpoints": [
|
||||
"https://app.gitbutler.com/releases/nightly/{{target}}-{{arch}}/{{current_version}}"
|
||||
],
|
||||
|
@ -27,7 +27,7 @@
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"dialog": true,
|
||||
"dialog": false,
|
||||
"endpoints": [
|
||||
"https://app.gitbutler.com/releases/release/{{target}}-{{arch}}/{{current_version}}"
|
||||
],
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ export type Project = {
|
||||
preferred_key: Key;
|
||||
ok_with_force_push: boolean;
|
||||
omit_certificate_check: boolean | undefined;
|
||||
use_diff_context: boolean | undefined;
|
||||
};
|
||||
|
||||
export class ProjectService {
|
||||
|
122
gitbutler-ui/src/lib/backend/updater.ts
Normal file
122
gitbutler-ui/src/lib/backend/updater.ts
Normal 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);
|
||||
}
|
@ -5,6 +5,7 @@ import { startWith, switchMap } from 'rxjs/operators';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { PullRequest } from '$lib/github/types';
|
||||
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import type { BranchController } from '$lib/vbranches/branchController';
|
||||
import type { VirtualBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
import type { Branch, RemoteBranch } from '$lib/vbranches/types';
|
||||
|
||||
@ -14,7 +15,8 @@ export class BranchService {
|
||||
constructor(
|
||||
private vbranchService: VirtualBranchService,
|
||||
remoteBranchService: RemoteBranchService,
|
||||
private githubService: GitHubService
|
||||
private githubService: GitHubService,
|
||||
private branchController: BranchController
|
||||
) {
|
||||
const vbranchesWithEmpty$ = vbranchService.branches$.pipe(startWith([]));
|
||||
const branchesWithEmpty$ = remoteBranchService.branches$.pipe(startWith([]));
|
||||
@ -47,7 +49,7 @@ export class BranchService {
|
||||
|
||||
// Push if local commits
|
||||
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 {
|
||||
newBranch = branch;
|
||||
}
|
||||
|
@ -49,6 +49,8 @@
|
||||
color var(--transition-fast),
|
||||
filter var(--transition-fast);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&.pop {
|
||||
color: var(--clr-theme-scale-pop-10);
|
||||
background: color-mix(
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts" async="true">
|
||||
import FullscreenLoading from './FullscreenLoading.svelte';
|
||||
import NewBranchDropZone from './NewBranchDropZone.svelte';
|
||||
import BranchLane from '$lib/components/BranchLane.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
@ -39,7 +40,7 @@
|
||||
{#if branchesError}
|
||||
<div class="p-4" data-tauri-drag-region>Something went wrong...</div>
|
||||
{:else if !branches}
|
||||
<div class="loading" data-tauri-drag-region><Icon name="spinner" /></div>
|
||||
<FullscreenLoading />
|
||||
{:else}
|
||||
<div
|
||||
class="board"
|
||||
@ -213,13 +214,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Empty board */
|
||||
|
||||
.empty-board {
|
||||
|
@ -69,15 +69,15 @@
|
||||
}
|
||||
}
|
||||
.branch-files__header {
|
||||
padding-top: var(--space-12);
|
||||
padding-top: var(--space-14);
|
||||
padding-bottom: var(--space-12);
|
||||
padding-left: var(--space-20);
|
||||
padding-right: var(--space-12);
|
||||
padding-left: var(--space-14);
|
||||
padding-right: var(--space-14);
|
||||
}
|
||||
.files-padding {
|
||||
padding-top: 0;
|
||||
padding-bottom: var(--space-12);
|
||||
padding-left: var(--space-12);
|
||||
padding-right: var(--space-12);
|
||||
padding-left: var(--space-14);
|
||||
padding-right: var(--space-14);
|
||||
}
|
||||
</style>
|
||||
|
@ -407,7 +407,7 @@
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
background: var(--clr-theme-container-pale);
|
||||
padding: var(--space-12);
|
||||
padding: var(--space-14);
|
||||
justify-content: space-between;
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
user-select: none;
|
||||
@ -452,7 +452,8 @@
|
||||
|
||||
.header__remote-branch {
|
||||
color: var(--clr-theme-scale-ntrl-50);
|
||||
padding-left: var(--space-4);
|
||||
padding-left: var(--space-2);
|
||||
padding-right: var(--space-2);
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
text-overflow: ellipsis;
|
||||
|
@ -139,7 +139,6 @@
|
||||
width: 100%;
|
||||
height: var(--space-20);
|
||||
background: var(--target-branch-background);
|
||||
/* background-color: red; */
|
||||
}
|
||||
.header__info {
|
||||
display: flex;
|
||||
@ -152,7 +151,7 @@
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
background: var(--clr-theme-container-pale);
|
||||
padding: var(--space-12);
|
||||
padding: var(--space-14);
|
||||
justify-content: space-between;
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
user-select: none;
|
||||
@ -171,7 +170,8 @@
|
||||
|
||||
.header__remote-branch {
|
||||
color: var(--clr-theme-scale-ntrl-50);
|
||||
padding-left: var(--space-4);
|
||||
padding-left: var(--space-2);
|
||||
padding-right: var(--space-2);
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
text-overflow: ellipsis;
|
||||
|
@ -4,10 +4,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import { onMount } from 'svelte';
|
||||
import type iconsJson from '$lib/icons/icons.json';
|
||||
|
||||
export let size: 'medium' | 'large' = 'medium';
|
||||
export let icon: keyof typeof iconsJson | undefined = undefined;
|
||||
export let iconAlign: 'left' | 'right' = 'right';
|
||||
export let color: ButtonColor = 'primary';
|
||||
@ -21,6 +23,7 @@
|
||||
export let grow = false;
|
||||
export let align: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'auto' = 'auto';
|
||||
export let help = '';
|
||||
export let width: number | undefined = undefined;
|
||||
|
||||
export let element: HTMLAnchorElement | HTMLButtonElement | HTMLElement | null = null;
|
||||
|
||||
@ -37,6 +40,8 @@
|
||||
|
||||
<button
|
||||
class={`btn ${className}`}
|
||||
class:medium={size == 'medium'}
|
||||
class:large={size == 'large'}
|
||||
class:error-outline={color == 'error' && kind == 'outlined'}
|
||||
class:primary-outline={color == 'primary' && kind == 'outlined'}
|
||||
class:warn-outline={color == 'warn' && kind == 'outlined'}
|
||||
@ -51,6 +56,7 @@
|
||||
class:grow
|
||||
class:not-clickable={notClickable}
|
||||
style:align-self={align}
|
||||
style:width={width ? pxToRem(width) : undefined}
|
||||
use:tooltip={help}
|
||||
bind:this={element}
|
||||
disabled={disabled || loading}
|
||||
@ -84,7 +90,9 @@
|
||||
min-width: var(--size-btn-m);
|
||||
background: transparent;
|
||||
transition: background-color var(--transition-fast);
|
||||
cursor: pointer;
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@ -99,6 +107,7 @@
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
&.not-clickable {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@ -177,4 +186,16 @@
|
||||
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>
|
||||
|
@ -26,6 +26,7 @@
|
||||
class="checkbox"
|
||||
class:small
|
||||
{value}
|
||||
id={name}
|
||||
{name}
|
||||
{disabled}
|
||||
/>
|
||||
@ -33,6 +34,7 @@
|
||||
<style lang="postcss">
|
||||
.checkbox {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: var(--space-16);
|
||||
height: var(--space-16);
|
||||
flex-shrink: 0;
|
||||
|
@ -3,19 +3,34 @@
|
||||
|
||||
export let padding: string = 'var(--space-16)';
|
||||
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 dispatch = createEventDispatcher<{
|
||||
const dispatchClick = createEventDispatcher<{
|
||||
click: void;
|
||||
}>();
|
||||
|
||||
const dispatchChange = createEventDispatcher<{
|
||||
change: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="clickable-card"
|
||||
class:has-top-radius={hasTopRadius}
|
||||
class:has-bottom-radius={hasBottomRadius}
|
||||
class:has-bottom-line={hasBottomLine}
|
||||
style="padding: {padding}"
|
||||
on:click={() => {
|
||||
dispatch('click');
|
||||
dispatchClick('click');
|
||||
|
||||
dispatchChange('change', checked);
|
||||
|
||||
checked = !checked;
|
||||
}}
|
||||
class:card-disabled={disabled}
|
||||
{disabled}
|
||||
@ -43,8 +58,8 @@
|
||||
.clickable-card {
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
border-radius: var(--radius-l);
|
||||
border: 1px solid var(--clr-theme-container-outline-light);
|
||||
border-left: 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);
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
@ -84,4 +99,21 @@
|
||||
display: flex;
|
||||
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>
|
||||
|
@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
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 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 { projectAiGenAutoBranchNamingEnabled } from '$lib/config/config';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
@ -47,87 +49,82 @@
|
||||
toasts.error('Failed to update project sync status');
|
||||
}
|
||||
};
|
||||
|
||||
const aiGenToggle = () => {
|
||||
$aiGenEnabled = !$aiGenEnabled;
|
||||
$aiGenAutoBranchNamingEnabled = $aiGenEnabled;
|
||||
};
|
||||
|
||||
const aiGenBranchNamesToggle = () => {
|
||||
$aiGenAutoBranchNamingEnabled = !$aiGenAutoBranchNamingEnabled;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="space-y-2">
|
||||
{#if user}
|
||||
<h2 class="text-xl">GitButler Cloud</h2>
|
||||
{#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
|
||||
"Generate message" button.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle checked={$aiGenEnabled} on:change={aiGenToggle} />
|
||||
</svelte:fragment>
|
||||
</ClickableCard>
|
||||
|
||||
<header>
|
||||
<span class="text-text-subdued"> Summary generation </span>
|
||||
</header>
|
||||
<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}
|
||||
on:change={aiGenBranchNamesToggle}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</ClickableCard>
|
||||
</div>
|
||||
|
||||
<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;
|
||||
$aiGenAutoBranchNamingEnabled = $aiGenEnabled;
|
||||
}}
|
||||
/>
|
||||
<label class="ml-2" for="sync">Enable branch and commit message generation.</label>
|
||||
</div>
|
||||
<div class="pl-4 pr-8 text-sm text-light-700 dark:text-dark-200">
|
||||
Uses OpenAI's API. If enabled, diffs will sent to OpenAI's servers when pressing the
|
||||
"Generate message" button.
|
||||
</div>
|
||||
<div class="flex flex-col space-x-3">
|
||||
<div class="flex flex-row items-center gap-x-1">
|
||||
<Checkbox
|
||||
name="sync"
|
||||
disabled={user === undefined || !$aiGenEnabled}
|
||||
checked={$aiGenAutoBranchNamingEnabled}
|
||||
on:change={() => {
|
||||
$aiGenAutoBranchNamingEnabled = !$aiGenAutoBranchNamingEnabled;
|
||||
}}
|
||||
/>
|
||||
<label class="ml-2" for="sync">Automatically generate branch names.</label>
|
||||
</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}
|
||||
<div class="api-link">
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="{PUBLIC_API_BASE_URL}projects/{project.api?.repository_id}"
|
||||
>Go to GitButler Cloud Project</Link
|
||||
>
|
||||
</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>
|
||||
|
||||
{#if project.api}
|
||||
<div class="flex flex-row justify-end space-x-2">
|
||||
<div class="p-1">
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="{PUBLIC_API_BASE_URL}projects/{project.api?.repository_id}"
|
||||
>Go to GitButler Cloud Project</Link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<Login {userService} />
|
||||
<Spacer />
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<WelcomeSigninAction {userService} />
|
||||
<Spacer />
|
||||
{/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>
|
||||
|
@ -154,7 +154,7 @@
|
||||
.commit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: default;
|
||||
|
||||
border-radius: var(--space-6);
|
||||
background-color: var(--clr-theme-container-light);
|
||||
border: 1px solid var(--clr-theme-container-outline-light);
|
||||
@ -173,10 +173,11 @@
|
||||
}
|
||||
|
||||
.commit__header {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-10);
|
||||
padding: var(--space-12);
|
||||
padding: var(--space-14);
|
||||
}
|
||||
|
||||
.is-commit-open {
|
||||
@ -263,7 +264,7 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-8);
|
||||
padding: var(--space-12);
|
||||
padding: var(--space-14);
|
||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||
}
|
||||
</style>
|
||||
|
@ -135,7 +135,7 @@
|
||||
on:focus={useAutoHeight}
|
||||
on:change={() => currentCommitMessage.set(commitMessage)}
|
||||
spellcheck={false}
|
||||
class="text-input commit-box__textarea"
|
||||
class="text-input text-base-body-13 commit-box__textarea"
|
||||
rows="1"
|
||||
disabled={isGeneratingCommigMessage}
|
||||
placeholder="Your commit message here"
|
||||
@ -217,7 +217,7 @@
|
||||
.commit-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--space-16);
|
||||
padding: var(--space-14);
|
||||
background: var(--clr-theme-container-light);
|
||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||
transition: background-color var(--transition-medium);
|
||||
|
@ -91,7 +91,7 @@
|
||||
.commit-list__content {
|
||||
display: flex;
|
||||
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);
|
||||
}
|
||||
.upstream-message {
|
||||
|
@ -35,7 +35,7 @@
|
||||
.header {
|
||||
display: flex;
|
||||
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;
|
||||
gap: var(--space-8);
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
<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 TextBox from '$lib/components/TextBox.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@ -14,36 +16,48 @@
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<form class="flex flex-col gap-3">
|
||||
<fieldset class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="path">Path</label>
|
||||
<TextBox readonly id="path" value={project?.path} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="name">Project Name</label>
|
||||
<TextBox
|
||||
id="name"
|
||||
placeholder="Project name can't be empty"
|
||||
bind:value={title}
|
||||
required
|
||||
on:change={(e) => {
|
||||
project.title = e.detail;
|
||||
dispatch('updated', project);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="description">Project Description</label>
|
||||
<TextArea
|
||||
id="description"
|
||||
rows={3}
|
||||
bind:value={description}
|
||||
on:change={(e) => {
|
||||
project.description = e.detail;
|
||||
dispatch('updated', project);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<SectionCard>
|
||||
<form>
|
||||
<fieldset class="fields-wrapper">
|
||||
<TextBox label="Path" readonly id="path" value={project?.path} />
|
||||
<section class="description-wrapper">
|
||||
<TextBox
|
||||
label="Project Name"
|
||||
id="name"
|
||||
placeholder="Project name can't be empty"
|
||||
bind:value={title}
|
||||
required
|
||||
on:change={(e) => {
|
||||
project.title = e.detail;
|
||||
dispatch('updated', project);
|
||||
}}
|
||||
/>
|
||||
<TextArea
|
||||
id="description"
|
||||
rows={3}
|
||||
placeholder="Project description"
|
||||
bind:value={description}
|
||||
on:change={(e) => {
|
||||
project.description = e.detail;
|
||||
dispatch('updated', project);
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
</fieldset>
|
||||
</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>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Button from './Button.svelte';
|
||||
import HunkViewer from './HunkViewer.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
|
||||
@ -30,6 +31,8 @@
|
||||
|
||||
$: maxLineNumber = sections[sections.length - 1]?.maxLineNumber;
|
||||
$: minWidth = getGutterMinWidth(maxLineNumber);
|
||||
|
||||
let alwaysShow = false;
|
||||
</script>
|
||||
|
||||
<div class="hunks">
|
||||
@ -37,6 +40,13 @@
|
||||
Binary content not shown
|
||||
{:else if isLarge}
|
||||
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}
|
||||
{#each sections as section}
|
||||
{@const { added, removed } = computeAddedRemovedByHunk(section)}
|
||||
|
@ -50,49 +50,46 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
on:click
|
||||
on:keydown
|
||||
on:dragstart={() => {
|
||||
// Reset selection if the file being dragged is not in the selected list
|
||||
if ($selectedFiles.length > 0 && !$selectedFiles.find((f) => f.id == file.id)) {
|
||||
$selectedFiles = [];
|
||||
}
|
||||
}}
|
||||
use:draggable={{
|
||||
...draggableFile(branchId, file, selectedFiles),
|
||||
disabled: readonly || isUnapplied,
|
||||
selector: '.selected-draggable'
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:contextmenu={(e) =>
|
||||
popupMenu.openByMouse(e, {
|
||||
files: $selectedFiles.includes(file) ? $selectedFiles : [file]
|
||||
})}
|
||||
>
|
||||
<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}
|
||||
role="listitem"
|
||||
on:contextmenu|preventDefault
|
||||
on:click
|
||||
on:keydown
|
||||
on:dragstart={() => {
|
||||
// Reset selection if the file being dragged is not in the selected list
|
||||
if ($selectedFiles.length > 0 && !$selectedFiles.find((f) => f.id == file.id)) {
|
||||
$selectedFiles = [];
|
||||
}
|
||||
}}
|
||||
use:draggable={{
|
||||
...draggableFile(branchId, file, selectedFiles),
|
||||
disabled: readonly || isUnapplied,
|
||||
selector: '.selected-draggable'
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:contextmenu={(e) =>
|
||||
popupMenu.openByMouse(e, {
|
||||
files: $selectedFiles.includes(file) ? $selectedFiles : [file]
|
||||
})}
|
||||
>
|
||||
<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">
|
||||
<img src={getVSIFileIcon(file.path)} alt="js" style="width: var(--space-12)" />
|
||||
<span class="text-base-12 name">
|
||||
@ -108,7 +105,14 @@
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.list-item-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--space-28);
|
||||
|
15
gitbutler-ui/src/lib/components/FullscreenLoading.svelte
Normal file
15
gitbutler-ui/src/lib/components/FullscreenLoading.svelte
Normal 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>
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts" context="module">
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
export type IconColor = 'success' | 'error' | 'pop' | 'warn' | 'neutral' | undefined;
|
||||
</script>
|
||||
|
||||
@ -10,8 +11,6 @@
|
||||
export let opacity: number | undefined = 1;
|
||||
export let spinnerRadius: number | undefined = 5;
|
||||
export let size = 16;
|
||||
|
||||
const pxToRem = (px: number) => `${px / 16}rem`;
|
||||
</script>
|
||||
|
||||
<svg
|
||||
|
@ -32,9 +32,9 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--clr-theme-scale-ntrl-40);
|
||||
border-radius: var(--radius-m);
|
||||
color: var(--clr-theme-scale-ntrl-50);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
@ -45,6 +45,7 @@
|
||||
}
|
||||
.selected {
|
||||
background-color: color-mix(in srgb, transparent, var(--darken-tint-light));
|
||||
cursor: default;
|
||||
}
|
||||
.large {
|
||||
height: var(--size-btn-l);
|
||||
|
@ -125,4 +125,8 @@
|
||||
:global(.info-message__text p:not(:last-child)) {
|
||||
margin-bottom: var(--space-10);
|
||||
}
|
||||
:global(.info-message__text ul) {
|
||||
list-style-type: circle;
|
||||
padding: 0 0 0 var(--space-16);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,8 +1,12 @@
|
||||
<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 Button from '$lib/components/Button.svelte';
|
||||
import Link from '$lib/components/Link.svelte';
|
||||
import TextBox from '$lib/components/TextBox.svelte';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import { debounce } from '$lib/utils/debounce';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@ -81,121 +85,199 @@
|
||||
preferred_key: 'generated'
|
||||
});
|
||||
}
|
||||
|
||||
let showPassphrase = false;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>Git Authentication</p>
|
||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
||||
<div>
|
||||
Configure the authentication flow for GitButler when authenticating with your Git remote
|
||||
provider.
|
||||
</div>
|
||||
</div>
|
||||
<section class="git-auth-wrap">
|
||||
<h3 class="text-base-15 text-bold">Git Authentication</h3>
|
||||
<p class="text-base-body-12">
|
||||
Configure the authentication flow for GitButler when authenticating with your Git remote
|
||||
provider.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2" style="grid-template-columns: max-content 1fr;">
|
||||
<input type="radio" bind:group={selectedOption} value="default" on:input={setDefaultKey} />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div>Auto detect</div>
|
||||
{#if selectedOption === 'default'}
|
||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
||||
<div>GitButler will attempt all available authentication flows automatically.</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<form>
|
||||
<fieldset class="git-radio">
|
||||
<ClickableCard
|
||||
hasBottomRadius={false}
|
||||
on:click={() => {
|
||||
if (selectedOption == 'default') return;
|
||||
|
||||
<input type="radio" bind:group={selectedOption} value="local" />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div>Use existing SSH key</div>
|
||||
selectedOption = 'default';
|
||||
setDefaultKey();
|
||||
}}
|
||||
>
|
||||
<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'}
|
||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
||||
Add the path to an existing SSH key that GitButler can use.
|
||||
</div>
|
||||
<SectionCard hasTopRadius={false} hasBottomRadius={false}>
|
||||
<div class="inputs-group">
|
||||
<TextBox
|
||||
label="Path to private key"
|
||||
placeholder="for example: ~/.ssh/id_rsa"
|
||||
bind:value={privateKeyPath}
|
||||
on:input={debounce(setLocalKey, 600)}
|
||||
/>
|
||||
|
||||
<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
|
||||
placeholder="for example: ~/.ssh/id_rsa"
|
||||
bind:value={privateKeyPath}
|
||||
on:input={debounce(setLocalKey, 600)}
|
||||
/>
|
||||
|
||||
<label for="passphrase">Passphrase (optional)</label>
|
||||
<TextBox
|
||||
type="password"
|
||||
bind:value={privateKeyPassphrase}
|
||||
on:input={debounce(setLocalKey, 600)}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-with-button">
|
||||
<TextBox
|
||||
label="Passphrase (optional)"
|
||||
type={showPassphrase ? 'text' : 'password'}
|
||||
bind:value={privateKeyPassphrase}
|
||||
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>
|
||||
</SectionCard>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input type="radio" bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="pr-8">
|
||||
<div>Use locally generated SSH key</div>
|
||||
</div>
|
||||
<ClickableCard
|
||||
hasTopRadius={false}
|
||||
hasBottomRadius={false}
|
||||
hasBottomLine={selectedOption !== 'generated'}
|
||||
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'}
|
||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
||||
GitButler will use a locally generated SSH key. For this to work you <b>need</b>
|
||||
to add the following public key to your Git remote provider:
|
||||
</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>
|
||||
<SectionCard hasTopRadius={false} hasBottomRadius={false}>
|
||||
<TextBox readonly selectall bind:value={sshKey} />
|
||||
<div class="row-buttons">
|
||||
<Button
|
||||
kind="filled"
|
||||
color="primary"
|
||||
icon="copy"
|
||||
on:click={() => copyToClipboard(sshKey)}
|
||||
disabled={selectedOption !== 'generated'}
|
||||
>
|
||||
Copy to Clipboard
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/settings/ssh/new"
|
||||
disabled={selectedOption !== 'generated'}
|
||||
<Button
|
||||
kind="outlined"
|
||||
color="neutral"
|
||||
icon="open-link"
|
||||
on:click={() => {
|
||||
open('https://github.com/settings/ssh/new');
|
||||
}}
|
||||
>
|
||||
Add key to GitHub
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={selectedOption}
|
||||
value="gitCredentialsHelper"
|
||||
on:input={setGitCredentialsHelperKey}
|
||||
/>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<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>
|
||||
</div>
|
||||
{#if selectedOption === 'gitCredentialsHelper'}
|
||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
||||
<ClickableCard
|
||||
hasTopRadius={false}
|
||||
on:click={() => {
|
||||
if (selectedOption == 'gitCredentialsHelper') return;
|
||||
|
||||
selectedOption = 'gitCredentialsHelper';
|
||||
setGitCredentialsHelperKey();
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="title">Use a Git credentials helper</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="body">
|
||||
GitButler will use the system's git credentials helper.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link target="_blank" rel="noreferrer" href="https://git-scm.com/doc/credential-helpers">
|
||||
Learn more
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
|
||||
<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>
|
||||
|
@ -1,5 +1,7 @@
|
||||
<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 { createEventDispatcher } from 'svelte';
|
||||
import type { Project } from '$lib/backend/projects';
|
||||
@ -16,57 +18,70 @@
|
||||
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>
|
||||
|
||||
<div class="flex flex-1 flex-col gap-1 rounded-lg border border-light-400 p-2 dark:border-dark-500">
|
||||
<form class="flex items-center gap-1">
|
||||
<Checkbox
|
||||
name="allow-force-pushing"
|
||||
checked={allowForcePushing}
|
||||
on:change={() => {
|
||||
allowForcePushing = !allowForcePushing;
|
||||
dispatch('updated', { ok_with_force_push: allowForcePushing });
|
||||
}}
|
||||
/>
|
||||
<label class="ml-2" for="allow-force-pushing">
|
||||
<div>Allow force pushing</div>
|
||||
</label>
|
||||
</form>
|
||||
<p class="ml-7 text-light-700 dark:text-dark-200">
|
||||
Force pushing allows GitButler to override branches even if they were pushed to remote. We will
|
||||
never force push to the trunk.
|
||||
</p>
|
||||
<section class="wrapper">
|
||||
<ClickableCard
|
||||
on:click={() => {
|
||||
allowForcePushing = !allowForcePushing;
|
||||
onAllowForcePushingChange();
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="title">Allow force pushing</svelte:fragment>
|
||||
<svelte:fragment slot="body">
|
||||
Force pushing allows GitButler to override branches even if they were pushed to remote. We
|
||||
will never force push to the trunk.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle bind:checked={allowForcePushing} on:change={onAllowForcePushingChange} />
|
||||
</svelte:fragment>
|
||||
</ClickableCard>
|
||||
|
||||
<form class="flex items-center gap-1">
|
||||
<Checkbox
|
||||
name="omit-certificate-check"
|
||||
checked={omitCertificateCheck}
|
||||
on:change={() => {
|
||||
omitCertificateCheck = !omitCertificateCheck;
|
||||
dispatch('updated', { omit_certificate_check: omitCertificateCheck });
|
||||
}}
|
||||
/>
|
||||
<label class="ml-2" for="allow-force-pushing">
|
||||
<div>Ignore host certificate checks</div>
|
||||
</label>
|
||||
</form>
|
||||
<p class="ml-7 text-light-700 dark:text-dark-200">
|
||||
Enabling this will ignore host certificate checks when authenticating with ssh.
|
||||
</p>
|
||||
<ClickableCard
|
||||
on:click={() => {
|
||||
omitCertificateCheck = !omitCertificateCheck;
|
||||
onOmitCertificateCheckChange();
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="title">Ignore host certificate checks</svelte:fragment>
|
||||
<svelte:fragment slot="body">
|
||||
Enabling this will ignore host certificate checks when authenticating with ssh.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle bind:checked={omitCertificateCheck} on:change={onOmitCertificateCheckChange} />
|
||||
</svelte:fragment>
|
||||
</ClickableCard>
|
||||
|
||||
<form class="flex items-center gap-1">
|
||||
<Checkbox
|
||||
name="run-commit-hooks"
|
||||
checked={$runCommitHooks}
|
||||
on:change={() => {
|
||||
$runCommitHooks = !$runCommitHooks;
|
||||
}}
|
||||
/>
|
||||
<label class="ml-2" for="allow-force-pushing">
|
||||
<div>Run commit hooks</div>
|
||||
</label>
|
||||
</form>
|
||||
<p class="ml-7 text-light-700 dark:text-dark-200">
|
||||
Enabling this will run any git pre and post commit hooks you have configured in your repository.
|
||||
</p>
|
||||
</div>
|
||||
<ClickableCard on:click={onRunCommitHooksChange}>
|
||||
<svelte:fragment slot="title">Run commit hooks</svelte:fragment>
|
||||
<svelte:fragment slot="body">
|
||||
Enabling this will run any git pre and post commit hooks you have configured in your
|
||||
repository.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Toggle bind:checked={$runCommitHooks} on:change={onRunCommitHooksChange} />
|
||||
</svelte:fragment>
|
||||
</ClickableCard>
|
||||
</section>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<style lang="post-css">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-8);
|
||||
}
|
||||
</style>
|
||||
|
@ -29,10 +29,14 @@
|
||||
(b) => b.name == 'origin/master' || b.name == 'origin/main'
|
||||
);
|
||||
|
||||
function onSetTargetClick() {
|
||||
async function onSetTargetClick() {
|
||||
if (!selectedBranch) return;
|
||||
loading = true;
|
||||
branchController.setTarget(selectedBranch.name).finally(() => (loading = false));
|
||||
try {
|
||||
await branchController.setTarget(selectedBranch.name);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
<style lang="postcss">
|
||||
.radio {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: var(--space-16);
|
||||
height: var(--space-16);
|
||||
border-radius: var(--space-16);
|
||||
|
@ -25,10 +25,10 @@
|
||||
modal.show();
|
||||
}}
|
||||
>
|
||||
Remove project …
|
||||
Remove project
|
||||
</Button>
|
||||
|
||||
<Modal bind:this={modal} title="Remove from GitButler">
|
||||
<Modal bind:this={modal}>
|
||||
<div class="remove-project-description">
|
||||
<p class="text-base-body-14">
|
||||
Are you sure you want to remove
|
||||
|
@ -1,9 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let orientation: 'row' | 'column' = 'column';
|
||||
export let hasTopRadius = true;
|
||||
export let hasBottomRadius = true;
|
||||
export let hasBottomLine = true;
|
||||
|
||||
const SLOTS = $$props.$$slots;
|
||||
</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}
|
||||
<div class="section-card__icon-side">
|
||||
<slot name="iconSide" />
|
||||
@ -32,8 +42,8 @@
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
padding: var(--space-16);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-theme-container-outline-light);
|
||||
border-left: 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);
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
@ -55,4 +65,21 @@
|
||||
.section-card__text {
|
||||
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>
|
||||
|
@ -84,6 +84,8 @@
|
||||
|
||||
transition: background var(--transition-fast);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
@ -98,11 +100,15 @@
|
||||
border-right-width: 1px;
|
||||
border-left-width: 1px;
|
||||
|
||||
cursor: default;
|
||||
|
||||
& > .label {
|
||||
color: var(--clr-theme-scale-ntrl-0);
|
||||
cursor: default;
|
||||
}
|
||||
& > .icon {
|
||||
color: var(--clr-theme-scale-ntrl-30);
|
||||
cursor: default;
|
||||
}
|
||||
&.left {
|
||||
border-right-width: 1px;
|
||||
@ -128,9 +134,11 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--clr-theme-scale-ntrl-50);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--clr-theme-scale-ntrl-40);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
@ -16,6 +16,7 @@
|
||||
.divider {
|
||||
height: 1px;
|
||||
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>
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
<button
|
||||
class="sync-btn"
|
||||
class:sync-btn-busy={$baseServiceBusy$}
|
||||
on:click={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -53,6 +54,12 @@
|
||||
background: var(--clr-theme-container-light);
|
||||
border: 1px solid var(--clr-theme-container-outline-light);
|
||||
border-radius: var(--radius-m);
|
||||
cursor: pointer;
|
||||
|
||||
&.sync-btn-busy {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
border var(--transition-fast);
|
||||
|
@ -79,7 +79,7 @@
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
.clickable {
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* colors */
|
||||
|
@ -10,31 +10,49 @@
|
||||
export let autocomplete: string | undefined = undefined;
|
||||
export let autocorrect: string | undefined = undefined;
|
||||
export let spellcheck = false;
|
||||
export let label: string | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{ input: string; change: string }>();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
class="text-input textarea"
|
||||
bind:value
|
||||
{disabled}
|
||||
{id}
|
||||
name={id}
|
||||
{placeholder}
|
||||
{required}
|
||||
{rows}
|
||||
{autocomplete}
|
||||
{autocorrect}
|
||||
{spellcheck}
|
||||
on:input={(e) => dispatch('input', e.currentTarget.value)}
|
||||
on:change={(e) => dispatch('change', e.currentTarget.value)}
|
||||
/>
|
||||
<div class="textarea-wrapper">
|
||||
{#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
|
||||
{disabled}
|
||||
{id}
|
||||
name={id}
|
||||
{placeholder}
|
||||
{required}
|
||||
{rows}
|
||||
{autocomplete}
|
||||
{autocorrect}
|
||||
{spellcheck}
|
||||
on:input={(e) => dispatch('input', e.currentTarget.value)}
|
||||
on:change={(e) => dispatch('change', e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.textarea-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
.textarea {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
padding-top: var(--space-12);
|
||||
padding-bottom: var(--space-12);
|
||||
}
|
||||
|
||||
.textbox__label {
|
||||
color: var(--clr-theme-scale-ntrl-50);
|
||||
}
|
||||
</style>
|
||||
|
@ -3,27 +3,29 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
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 icon: keyof typeof iconsJson | undefined = undefined;
|
||||
export let value: string | undefined = undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let label: string | undefined = undefined;
|
||||
export let reversedDirection: boolean = false;
|
||||
export let wide: boolean = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let required = false;
|
||||
export let noselect = false;
|
||||
export let selectall = false;
|
||||
export let element: HTMLElement | undefined = undefined;
|
||||
export let spellcheck = false;
|
||||
|
||||
export let type: 'text' | 'password' | 'select' = 'text';
|
||||
|
||||
const dispatch = createEventDispatcher<{ input: string; change: string }>();
|
||||
</script>
|
||||
|
||||
<div class="textbox" bind:this={element}>
|
||||
<div class="textbox" bind:this={element} class:wide>
|
||||
{#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>
|
||||
{/if}
|
||||
@ -38,6 +40,7 @@
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
{id}
|
||||
{readonly}
|
||||
@ -47,7 +50,7 @@
|
||||
{disabled}
|
||||
{...{ type }}
|
||||
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-all={selectall}
|
||||
bind:value
|
||||
@ -71,6 +74,8 @@
|
||||
}
|
||||
|
||||
.textbox__input {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
height: var(--size-btn-l);
|
||||
width: 100%;
|
||||
@ -82,11 +87,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.textbox__input[type='select'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.textbox__label {
|
||||
color: var(--clr-theme-scale-ntrl-50);
|
||||
}
|
||||
|
||||
.textbox__icon {
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@ -118,4 +128,9 @@
|
||||
background-color: var(--clr-theme-container-pale);
|
||||
border-color: var(--clr-theme-container-outline-light);
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
@ -58,6 +58,7 @@
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -78,14 +79,14 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--radius-l);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-theme-container-outline-light);
|
||||
overflow: hidden;
|
||||
|
||||
& img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
height: auto;
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +115,10 @@
|
||||
|
||||
.theme-card.selected .theme-card__preview {
|
||||
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 {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let name = '';
|
||||
|
||||
@ -8,12 +9,18 @@
|
||||
export let checked = false;
|
||||
export let value = '';
|
||||
export let help = '';
|
||||
|
||||
let input: HTMLInputElement;
|
||||
const dispatch = createEventDispatcher<{ change: boolean }>();
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={input}
|
||||
bind:checked
|
||||
on:click|stopPropagation
|
||||
on:change
|
||||
use:tooltip={help}
|
||||
on:change={() => {
|
||||
dispatch('change', checked);
|
||||
}}
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
class:small
|
||||
@ -21,12 +28,13 @@
|
||||
{name}
|
||||
id={name}
|
||||
{disabled}
|
||||
bind:checked
|
||||
use:tooltip={help}
|
||||
/>
|
||||
|
||||
<style lang="postcss">
|
||||
.toggle {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: calc(var(--space-24) + var(--space-2));
|
||||
height: var(--space-16);
|
||||
border-radius: var(--space-16);
|
||||
|
311
gitbutler-ui/src/lib/components/UpdateButton.svelte
Normal file
311
gitbutler-ui/src/lib/components/UpdateButton.svelte
Normal 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>
|
@ -31,14 +31,15 @@
|
||||
<Icon name={icon} />
|
||||
{/if}
|
||||
|
||||
<div class="label text-base-12">
|
||||
<span class="label text-base-12">
|
||||
{label}
|
||||
</div>
|
||||
</span>
|
||||
<slot name="control" />
|
||||
</button>
|
||||
|
||||
<style lang="postcss">
|
||||
.menu-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
@ -54,6 +55,7 @@
|
||||
}
|
||||
}
|
||||
.label {
|
||||
user-select: none;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -6,13 +6,15 @@
|
||||
|
||||
<section class="content-wrapper">
|
||||
<ScrollableContainer>
|
||||
<div class="content" data-tauri-drag-region>
|
||||
{#if title}
|
||||
<h1 class="title text-head-24">
|
||||
{title}
|
||||
</h1>
|
||||
{/if}
|
||||
<slot />
|
||||
<div class="drag-region" data-tauri-drag-region>
|
||||
<div class="content" data-tauri-drag-region>
|
||||
{#if title}
|
||||
<h1 class="title text-head-24">
|
||||
{title}
|
||||
</h1>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
</section>
|
||||
@ -23,7 +25,12 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
background-color: var(--clr-theme-container-light);
|
||||
background-color: var(--clr-theme-container-pale);
|
||||
}
|
||||
|
||||
.drag-region {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
@ -119,7 +119,7 @@
|
||||
justify-content: space-between;
|
||||
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);
|
||||
background-color: var(--clr-theme-container-pale);
|
||||
background-color: var(--clr-theme-container-light);
|
||||
height: 100%;
|
||||
width: 16rem;
|
||||
}
|
||||
@ -172,7 +172,7 @@
|
||||
transition: none;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--clr-theme-container-pale),
|
||||
var(--clr-theme-container-light),
|
||||
var(--darken-tint-light)
|
||||
);
|
||||
}
|
||||
@ -183,7 +183,11 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@ -209,7 +213,7 @@
|
||||
padding: var(--space-16);
|
||||
border-radius: var(--radius-m);
|
||||
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);
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
type PrStatus,
|
||||
MergeMethod
|
||||
} 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 * as toasts from '$lib/utils/toasts';
|
||||
import lscache from 'lscache';
|
||||
@ -496,7 +496,7 @@ function loadPrs(
|
||||
* "status": 422
|
||||
* }
|
||||
*/
|
||||
function mapErrorToToast(err: any): ToastMessage | undefined {
|
||||
function mapErrorToToast(err: any): Toast | undefined {
|
||||
// We expect an object to be thrown by octokit.
|
||||
if (typeof err != 'object') return;
|
||||
|
||||
|
@ -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",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
bottom: var(--space-20);
|
||||
right: var(--space-20);
|
||||
gap: var(--space-8);
|
||||
max-width: 22rem;
|
||||
max-width: 30rem;
|
||||
z-index: 50;
|
||||
}
|
||||
</style>
|
||||
|
@ -2,22 +2,26 @@ import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
export type ToastStyle = 'neutral' | 'error' | 'pop' | 'warn';
|
||||
|
||||
export interface ToastMessage {
|
||||
id?: number;
|
||||
export interface Toast {
|
||||
id?: string;
|
||||
message: string;
|
||||
title?: string;
|
||||
style?: ToastStyle;
|
||||
}
|
||||
|
||||
export const toastStore: Writable<ToastMessage[]> = writable([]);
|
||||
export const toastStore: Writable<Toast[]> = writable([]);
|
||||
|
||||
let idCounter = 0;
|
||||
|
||||
export function showToast(message: ToastMessage) {
|
||||
message.message = message.message.replace(/^ */gm, '');
|
||||
toastStore.update((items) => [...items, { id: idCounter++, ...message }]);
|
||||
export function showToast(toast: Toast) {
|
||||
toast.message = toast.message.replace(/^ */gm, '');
|
||||
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));
|
||||
}
|
||||
|
1
gitbutler-ui/src/lib/utils/pxToRem.ts
Normal file
1
gitbutler-ui/src/lib/utils/pxToRem.ts
Normal file
@ -0,0 +1 @@
|
||||
export const pxToRem = (px: number, base: number = 16) => `${px / base}rem`;
|
@ -1,4 +1,5 @@
|
||||
import { invoke } from '$lib/backend/ipc';
|
||||
import { showToast } from '$lib/notifications/toasts';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import posthog from 'posthog-js';
|
||||
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
@ -61,7 +62,7 @@ export class BranchController {
|
||||
});
|
||||
posthog.capture('Commit Successful');
|
||||
} catch (err: any) {
|
||||
toasts.error('Failed to commit branch');
|
||||
toasts.error('Failed to commit changes');
|
||||
posthog.capture('Commit Failed', err);
|
||||
}
|
||||
}
|
||||
@ -193,10 +194,33 @@ export class BranchController {
|
||||
await this.vbranchService.reload();
|
||||
return await this.vbranchService.getById(branchId);
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
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 {
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
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 { plainToInstance } from 'class-transformer';
|
||||
import posthog from 'posthog-js';
|
||||
import {
|
||||
switchMap,
|
||||
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) {
|
||||
@ -147,6 +126,9 @@ export class BaseBranchService {
|
||||
return await getBaseBranch({ projectId });
|
||||
}),
|
||||
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),
|
||||
catchError((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.
|
||||
await invoke<void>('fetch_from_target', { projectId: this.projectId });
|
||||
} 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?');
|
||||
} else {
|
||||
toasts.error(`Failed to fetch branch: ${err.message}`);
|
||||
}
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,6 +290,13 @@ export class BaseBranch {
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
import '../styles/main.postcss';
|
||||
|
||||
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
|
||||
import UpdateButton from '$lib/components/UpdateButton.svelte';
|
||||
import ToastController from '$lib/notifications/ToastController.svelte';
|
||||
import { SETTINGS_CONTEXT, loadUserSettings } from '$lib/settings/userSettings';
|
||||
import * as events from '$lib/utils/events';
|
||||
@ -14,7 +15,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data: LayoutData;
|
||||
const { cloud, user$ } = data;
|
||||
const { cloud, user$, updaterService } = data;
|
||||
|
||||
const userSettings = loadUserSettings();
|
||||
initTheme(userSettings);
|
||||
@ -51,3 +52,4 @@
|
||||
<Toaster />
|
||||
<ShareIssueModal bind:this={shareIssueModal} user={$user$} {cloud} />
|
||||
<ToastController />
|
||||
<UpdateButton {updaterService} />
|
||||
|
@ -2,6 +2,7 @@ import { initPostHog } from '$lib/analytics/posthog';
|
||||
import { initSentry } from '$lib/analytics/sentry';
|
||||
import { getCloudApiClient } from '$lib/backend/cloud';
|
||||
import { ProjectService } from '$lib/backend/projects';
|
||||
import { UpdaterService } from '$lib/backend/updater';
|
||||
import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import lscache from 'lscache';
|
||||
@ -32,6 +33,7 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
|
||||
if (enabled) initPostHog();
|
||||
});
|
||||
const userService = new UserService();
|
||||
const updaterService = new UpdaterService();
|
||||
|
||||
// TODO: Find a workaround to avoid this dynamic import
|
||||
// https://github.com/sveltejs/kit/issues/905
|
||||
@ -41,6 +43,7 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
|
||||
return {
|
||||
projectService: new ProjectService(defaultPath),
|
||||
cloud: getCloudApiClient({ fetch: realFetch }),
|
||||
updaterService,
|
||||
userService,
|
||||
user$: userService.user$
|
||||
};
|
||||
|
@ -9,11 +9,8 @@
|
||||
import * as hotkeys from '$lib/utils/hotkeys';
|
||||
import { unsubscribe } from '$lib/utils/random';
|
||||
import { getRemoteBranches } from '$lib/vbranches/branchStoresCache';
|
||||
import { interval, Subscription } from 'rxjs';
|
||||
import { startWith, tap } from 'rxjs/operators';
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
@ -35,28 +32,31 @@
|
||||
$: user$ = data.user$;
|
||||
|
||||
let trayViewport: HTMLElement;
|
||||
let intervalId: any;
|
||||
handleMenuActions(data.projectId);
|
||||
|
||||
let lastProjectId: string | undefined = undefined;
|
||||
onMount(() => {
|
||||
let fetchSub: Subscription;
|
||||
// Project is auto-fetched on page load and then every 15 minutes
|
||||
page.subscribe((page) => {
|
||||
if (page.params.projectId !== lastProjectId) {
|
||||
lastProjectId = page.params.projectId;
|
||||
fetchSub?.unsubscribe();
|
||||
fetchSub = interval(1000 * 60 * 15)
|
||||
.pipe(
|
||||
startWith(0),
|
||||
tap(() => baseBranchService.fetchFromTarget())
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
});
|
||||
return unsubscribe(
|
||||
// Once on load and every time the project id changes
|
||||
$: if (projectId) setupFetchInterval();
|
||||
|
||||
function setupFetchInterval() {
|
||||
baseBranchService.fetchFromTarget();
|
||||
clearFetchInterval();
|
||||
intervalId = setInterval(() => baseBranchService.fetchFromTarget(), 15 * 60 * 1000);
|
||||
}
|
||||
|
||||
function clearFetchInterval() {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
}
|
||||
|
||||
onMount(() =>
|
||||
unsubscribe(
|
||||
menuSubscribe(data.projectId),
|
||||
hotkeys.on('Meta+Shift+S', () => syncToCloud($project$?.id))
|
||||
);
|
||||
hotkeys.on('Meta+Shift+S', () => syncToCloud(projectId))
|
||||
)
|
||||
);
|
||||
|
||||
onDestroy(() => {
|
||||
clearFetchInterval();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -65,8 +65,7 @@
|
||||
{:else if $baseError$}
|
||||
<ProblemLoadingRepo {projectService} {userService} project={$project$} error={$baseError$} />
|
||||
{:else if $baseBranch$ === null}
|
||||
{@const remoteBranches = getRemoteBranches(projectId)}
|
||||
{#await remoteBranches}
|
||||
{#await getRemoteBranches(projectId)}
|
||||
<p>loading...</p>
|
||||
{:then remoteBranches}
|
||||
{#if remoteBranches.length == 0}
|
||||
|
@ -34,7 +34,12 @@ export const load: LayoutLoad = async ({ params, parent }) => {
|
||||
);
|
||||
|
||||
const githubService = new GitHubService(userService, baseBranchService);
|
||||
const branchService = new BranchService(vbranchService, remoteBranchService, githubService);
|
||||
const branchService = new BranchService(
|
||||
vbranchService,
|
||||
remoteBranchService,
|
||||
githubService,
|
||||
branchController
|
||||
);
|
||||
|
||||
return {
|
||||
projectId,
|
||||
|
@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import CloudForm from '$lib/components/CloudForm.svelte';
|
||||
import DetailsForm from '$lib/components/DetailsForm.svelte';
|
||||
import FullscreenLoading from '$lib/components/FullscreenLoading.svelte';
|
||||
import KeysForm from '$lib/components/KeysForm.svelte';
|
||||
import PreferencesForm from '$lib/components/PreferencesForm.svelte';
|
||||
import RemoveProjectButton from '$lib/components/RemoveProjectButton.svelte';
|
||||
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte';
|
||||
import Spacer from '$lib/components/Spacer.svelte';
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import type { UserError } from '$lib/backend/ipc';
|
||||
import type { Key, Project } from '$lib/backend/projects';
|
||||
@ -66,74 +67,28 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<ScrollableContainer wide>
|
||||
<div class="settings" data-tauri-drag-region>
|
||||
<div class="card">
|
||||
{#if !$project$}
|
||||
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} />
|
||||
<Spacer margin={2} />
|
||||
<DetailsForm project={$project$} on:updated={onDetailsUpdated} />
|
||||
<Spacer margin={2} />
|
||||
<KeysForm project={$project$} on:updated={onKeysUpdated} />
|
||||
<Spacer margin={2} />
|
||||
<PreferencesForm project={$project$} on:updated={onPreferencesUpdated} />
|
||||
<Spacer margin={2} />
|
||||
|
||||
<div class="flex gap-x-4">
|
||||
<a
|
||||
href="https://discord.gg/wDKZCPEjXC"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
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
|
||||
bind:this={deleteConfirmationModal}
|
||||
projectTitle={$project$?.title}
|
||||
{isDeleting}
|
||||
{onDeleteClicked}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
{#if !$project$}
|
||||
<FullscreenLoading />
|
||||
{:else}
|
||||
<ContentWrapper title="Project settings">
|
||||
<CloudForm project={$project$} user={$user$} {userService} on:updated={onCloudUpdated} />
|
||||
<DetailsForm project={$project$} on:updated={onDetailsUpdated} />
|
||||
<KeysForm project={$project$} on:updated={onKeysUpdated} />
|
||||
<PreferencesForm project={$project$} on:updated={onPreferencesUpdated} />
|
||||
<SectionCard>
|
||||
<svelte:fragment slot="title">Remove project</svelte:fragment>
|
||||
<svelte:fragment slot="body">
|
||||
You can remove projects from GitButler, your code remains safe as this only clears
|
||||
configuration.
|
||||
</svelte:fragment>
|
||||
<div>
|
||||
<RemoveProjectButton
|
||||
bind:this={deleteConfirmationModal}
|
||||
projectTitle={$project$?.title}
|
||||
{isDeleting}
|
||||
{onDeleteClicked}
|
||||
/>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</ContentWrapper>
|
||||
{/if}
|
||||
|
@ -71,14 +71,6 @@ button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
div,
|
||||
button,
|
||||
[role='button'],
|
||||
select,
|
||||
a {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* SCROLL BAR STYLING */
|
||||
/* We don't use REM here becasue we don't want
|
||||
the scrollbar to scale with the font size */
|
||||
|
Loading…
Reference in New Issue
Block a user