mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-25 10:33:21 +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",
|
"rust-lang.rust-analyzer",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"ZixuanChen.vitest-explorer",
|
|
||||||
"EditorConfig.EditorConfig"
|
"EditorConfig.EditorConfig"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ impl Default for Options {
|
|||||||
pub fn workdir(
|
pub fn workdir(
|
||||||
repository: &Repository,
|
repository: &Repository,
|
||||||
commit_oid: &git::Oid,
|
commit_oid: &git::Oid,
|
||||||
|
context_lines: u32,
|
||||||
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
|
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
|
||||||
let commit = repository
|
let commit = repository
|
||||||
.find_commit(*commit_oid)
|
.find_commit(*commit_oid)
|
||||||
@ -72,7 +73,7 @@ pub fn workdir(
|
|||||||
.show_binary(true)
|
.show_binary(true)
|
||||||
.show_untracked_content(true)
|
.show_untracked_content(true)
|
||||||
.ignore_submodules(true)
|
.ignore_submodules(true)
|
||||||
.context_lines(0);
|
.context_lines(context_lines);
|
||||||
|
|
||||||
let diff = repository.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?;
|
let diff = repository.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?;
|
||||||
|
|
||||||
@ -83,6 +84,7 @@ pub fn trees(
|
|||||||
repository: &Repository,
|
repository: &Repository,
|
||||||
old_tree: &git::Tree,
|
old_tree: &git::Tree,
|
||||||
new_tree: &git::Tree,
|
new_tree: &git::Tree,
|
||||||
|
context_lines: u32,
|
||||||
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
|
) -> Result<HashMap<path::PathBuf, Vec<Hunk>>> {
|
||||||
let mut diff_opts = git2::DiffOptions::new();
|
let mut diff_opts = git2::DiffOptions::new();
|
||||||
diff_opts
|
diff_opts
|
||||||
@ -90,7 +92,7 @@ pub fn trees(
|
|||||||
.include_untracked(true)
|
.include_untracked(true)
|
||||||
.show_binary(true)
|
.show_binary(true)
|
||||||
.ignore_submodules(true)
|
.ignore_submodules(true)
|
||||||
.context_lines(0)
|
.context_lines(context_lines)
|
||||||
.show_untracked_content(true);
|
.show_untracked_content(true);
|
||||||
|
|
||||||
let diff =
|
let diff =
|
||||||
@ -340,7 +342,7 @@ mod tests {
|
|||||||
|
|
||||||
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
||||||
|
|
||||||
let diff = workdir(&repository, &head_commit_id).unwrap();
|
let diff = workdir(&repository, &head_commit_id, 0).unwrap();
|
||||||
assert_eq!(diff.len(), 1);
|
assert_eq!(diff.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
diff[&path::PathBuf::from("file")],
|
diff[&path::PathBuf::from("file")],
|
||||||
@ -363,7 +365,7 @@ mod tests {
|
|||||||
|
|
||||||
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
||||||
|
|
||||||
let diff = workdir(&repository, &head_commit_id).unwrap();
|
let diff = workdir(&repository, &head_commit_id, 0).unwrap();
|
||||||
assert_eq!(diff.len(), 1);
|
assert_eq!(diff.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
diff[&path::PathBuf::from("first")],
|
diff[&path::PathBuf::from("first")],
|
||||||
@ -387,7 +389,7 @@ mod tests {
|
|||||||
|
|
||||||
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
||||||
|
|
||||||
let diff = workdir(&repository, &head_commit_id).unwrap();
|
let diff = workdir(&repository, &head_commit_id, 0).unwrap();
|
||||||
assert_eq!(diff.len(), 2);
|
assert_eq!(diff.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
diff[&path::PathBuf::from("first")],
|
diff[&path::PathBuf::from("first")],
|
||||||
@ -431,7 +433,7 @@ mod tests {
|
|||||||
|
|
||||||
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
||||||
|
|
||||||
let diff = workdir(&repository, &head_commit_id).unwrap();
|
let diff = workdir(&repository, &head_commit_id, 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
diff[&path::PathBuf::from("image")],
|
diff[&path::PathBuf::from("image")],
|
||||||
vec![Hunk {
|
vec![Hunk {
|
||||||
@ -474,7 +476,7 @@ mod tests {
|
|||||||
|
|
||||||
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
let head_commit_id = repository.head().unwrap().peel_to_commit().unwrap().id();
|
||||||
|
|
||||||
let diff = workdir(&repository, &head_commit_id).unwrap();
|
let diff = workdir(&repository, &head_commit_id, 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
diff[&path::PathBuf::from("file")],
|
diff[&path::PathBuf::from("file")],
|
||||||
vec![Hunk {
|
vec![Hunk {
|
||||||
|
@ -365,6 +365,13 @@ impl Repository {
|
|||||||
if self.project.omit_certificate_check.unwrap_or(false) {
|
if self.project.omit_certificate_check.unwrap_or(false) {
|
||||||
cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk));
|
cbs.certificate_check(|_, _| Ok(git2::CertificateCheckStatus::CertificateOk));
|
||||||
}
|
}
|
||||||
|
cbs.push_update_reference(|_reference: &str, status: Option<&str>| {
|
||||||
|
if let Some(status) = status {
|
||||||
|
return Err(git2::Error::from_str(status));
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
match remote.push(
|
match remote.push(
|
||||||
&[refspec.as_str()],
|
&[refspec.as_str()],
|
||||||
Some(&mut git2::PushOptions::new().remote_callbacks(cbs)),
|
Some(&mut git2::PushOptions::new().remote_callbacks(cbs)),
|
||||||
|
@ -70,6 +70,10 @@ impl From<controller::AddError> for Error {
|
|||||||
code: Code::Projects,
|
code: Code::Projects,
|
||||||
message: "Path not found".to_string(),
|
message: "Path not found".to_string(),
|
||||||
},
|
},
|
||||||
|
controller::AddError::SubmodulesNotSupported => Error::UserError {
|
||||||
|
code: Code::Projects,
|
||||||
|
message: "Repositories with git submodules are not supported".to_string(),
|
||||||
|
},
|
||||||
controller::AddError::User(error) => error.into(),
|
controller::AddError::User(error) => error.into(),
|
||||||
controller::AddError::Other(error) => {
|
controller::AddError::Other(error) => {
|
||||||
tracing::error!(?error, "failed to add project");
|
tracing::error!(?error, "failed to add project");
|
||||||
|
@ -66,6 +66,10 @@ impl Controller {
|
|||||||
return Err(AddError::NotAGitRepository);
|
return Err(AddError::NotAGitRepository);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if path.join(".gitmodules").exists() {
|
||||||
|
return Err(AddError::SubmodulesNotSupported);
|
||||||
|
}
|
||||||
|
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
// title is the base name of the file
|
// title is the base name of the file
|
||||||
@ -79,6 +83,7 @@ impl Controller {
|
|||||||
title,
|
title,
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
api: None,
|
api: None,
|
||||||
|
use_diff_context: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -201,6 +206,10 @@ impl Controller {
|
|||||||
tracing::error!(project_id = %id, ?error, "failed to remove project data",);
|
tracing::error!(project_id = %id, ?error, "failed to remove project data",);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(error) = std::fs::remove_file(project.path.join(".git/gitbutler.json")) {
|
||||||
|
tracing::error!(project_id = %project.id, ?error, "failed to remove .git/gitbutler.json data",);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,6 +262,8 @@ pub enum AddError {
|
|||||||
PathNotFound,
|
PathNotFound,
|
||||||
#[error("project already exists")]
|
#[error("project already exists")]
|
||||||
AlreadyExists,
|
AlreadyExists,
|
||||||
|
#[error("submodules not supported")]
|
||||||
|
SubmodulesNotSupported,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
User(#[from] users::GetError),
|
User(#[from] users::GetError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -78,6 +78,8 @@ pub struct Project {
|
|||||||
pub project_data_last_fetch: Option<FetchResult>,
|
pub project_data_last_fetch: Option<FetchResult>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub omit_certificate_check: Option<bool>,
|
pub omit_certificate_check: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub use_diff_context: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<Project> for Project {
|
impl AsRef<Project> for Project {
|
||||||
|
@ -47,6 +47,7 @@ pub struct UpdateRequest {
|
|||||||
pub gitbutler_code_push_state: Option<project::CodePushState>,
|
pub gitbutler_code_push_state: Option<project::CodePushState>,
|
||||||
pub project_data_last_fetched: Option<project::FetchResult>,
|
pub project_data_last_fetched: Option<project::FetchResult>,
|
||||||
pub omit_certificate_check: Option<bool>,
|
pub omit_certificate_check: Option<bool>,
|
||||||
|
pub use_diff_context: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@ -139,6 +140,10 @@ impl Storage {
|
|||||||
project.omit_certificate_check = Some(omit_certificate_check);
|
project.omit_certificate_check = Some(omit_certificate_check);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(use_diff_context) = update_request.use_diff_context {
|
||||||
|
project.use_diff_context = Some(use_diff_context);
|
||||||
|
}
|
||||||
|
|
||||||
self.storage
|
self.storage
|
||||||
.write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?;
|
.write(PROJECTS_FILE, &serde_json::to_string_pretty(&projects)?)?;
|
||||||
|
|
||||||
|
@ -135,7 +135,12 @@ pub fn set_base_branch(
|
|||||||
// if there are any commits on the head branch or uncommitted changes in the working directory, we need to
|
// if there are any commits on the head branch or uncommitted changes in the working directory, we need to
|
||||||
// put them into a virtual branch
|
// put them into a virtual branch
|
||||||
|
|
||||||
let wd_diff = diff::workdir(repo, ¤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 {
|
if !wd_diff.is_empty() || current_head_commit.id() != target.sha {
|
||||||
let hunks_by_filepath =
|
let hunks_by_filepath =
|
||||||
super::virtual_hunks_by_filepath(&project_repository.project().path, &wd_diff);
|
super::virtual_hunks_by_filepath(&project_repository.project().path, &wd_diff);
|
||||||
@ -308,6 +313,12 @@ pub fn update_base_branch(
|
|||||||
let branch_writer =
|
let branch_writer =
|
||||||
branch::Writer::new(gb_repository).context("failed to create branch writer")?;
|
branch::Writer::new(gb_repository).context("failed to create branch writer")?;
|
||||||
|
|
||||||
|
let use_context = project_repository
|
||||||
|
.project()
|
||||||
|
.use_diff_context
|
||||||
|
.unwrap_or(false);
|
||||||
|
let context_lines = if use_context { 3_u32 } else { 0_u32 };
|
||||||
|
|
||||||
// try to update every branch
|
// try to update every branch
|
||||||
let updated_vbranches = super::get_status_by_branch(gb_repository, project_repository)?
|
let updated_vbranches = super::get_status_by_branch(gb_repository, project_repository)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -341,6 +352,7 @@ pub fn update_base_branch(
|
|||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
&branch_head_tree,
|
&branch_head_tree,
|
||||||
&branch_tree,
|
&branch_tree,
|
||||||
|
context_lines,
|
||||||
)?;
|
)?;
|
||||||
if non_commited_files.is_empty() {
|
if non_commited_files.is_empty() {
|
||||||
// if there are no commited files, then the branch is fully merged
|
// if there are no commited files, then the branch is fully merged
|
||||||
|
@ -12,7 +12,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
branch::BranchId,
|
branch::{self, BranchId},
|
||||||
controller::{Controller, ControllerError},
|
controller::{Controller, ControllerError},
|
||||||
BaseBranch, RemoteBranchFile,
|
BaseBranch, RemoteBranchFile,
|
||||||
};
|
};
|
||||||
@ -80,11 +80,23 @@ pub async fn list_virtual_branches(
|
|||||||
code: Code::Validation,
|
code: Code::Validation,
|
||||||
message: "Malformed project id".to_string(),
|
message: "Malformed project id".to_string(),
|
||||||
})?;
|
})?;
|
||||||
let branches = handle
|
let (branches, uses_diff_context) = handle
|
||||||
.state::<Controller>()
|
.state::<Controller>()
|
||||||
.list_virtual_branches(&project_id)
|
.list_virtual_branches(&project_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Migration: If use_diff_context is not already set and if there are no vbranches, set use_diff_context to true
|
||||||
|
if !uses_diff_context && branches.is_empty() {
|
||||||
|
let _ = handle
|
||||||
|
.state::<projects::Controller>()
|
||||||
|
.update(&projects::UpdateRequest {
|
||||||
|
id: project_id,
|
||||||
|
use_diff_context: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
let proxy = handle.state::<assets::Proxy>();
|
let proxy = handle.state::<assets::Proxy>();
|
||||||
let branches = proxy.proxy_virtual_branches(branches).await;
|
let branches = proxy.proxy_virtual_branches(branches).await;
|
||||||
Ok(branches)
|
Ok(branches)
|
||||||
|
@ -124,7 +124,8 @@ impl Controller {
|
|||||||
pub async fn list_virtual_branches(
|
pub async fn list_virtual_branches(
|
||||||
&self,
|
&self,
|
||||||
project_id: &ProjectId,
|
project_id: &ProjectId,
|
||||||
) -> Result<Vec<super::VirtualBranch>, ControllerError<errors::ListVirtualBranchesError>> {
|
) -> Result<(Vec<super::VirtualBranch>, bool), ControllerError<errors::ListVirtualBranchesError>>
|
||||||
|
{
|
||||||
self.inner(project_id)
|
self.inner(project_id)
|
||||||
.await
|
.await
|
||||||
.list_virtual_branches(project_id)
|
.list_virtual_branches(project_id)
|
||||||
@ -493,7 +494,8 @@ impl ControllerInner {
|
|||||||
pub async fn list_virtual_branches(
|
pub async fn list_virtual_branches(
|
||||||
&self,
|
&self,
|
||||||
project_id: &ProjectId,
|
project_id: &ProjectId,
|
||||||
) -> Result<Vec<super::VirtualBranch>, ControllerError<errors::ListVirtualBranchesError>> {
|
) -> Result<(Vec<super::VirtualBranch>, bool), ControllerError<errors::ListVirtualBranchesError>>
|
||||||
|
{
|
||||||
let _permit = self.semaphore.acquire().await;
|
let _permit = self.semaphore.acquire().await;
|
||||||
|
|
||||||
self.with_verify_branch(project_id, |gb_repository, project_repository, _| {
|
self.with_verify_branch(project_id, |gb_repository, project_repository, _| {
|
||||||
@ -570,8 +572,16 @@ impl ControllerInner {
|
|||||||
) -> Result<Vec<RemoteBranchFile>, Error> {
|
) -> Result<Vec<RemoteBranchFile>, Error> {
|
||||||
let project = self.projects.get(project_id)?;
|
let project = self.projects.get(project_id)?;
|
||||||
let project_repository = project_repository::Repository::open(&project)?;
|
let project_repository = project_repository::Repository::open(&project)?;
|
||||||
|
let use_context = project_repository
|
||||||
super::list_remote_commit_files(&project_repository.git_repository, commit_oid)
|
.project()
|
||||||
|
.use_diff_context
|
||||||
|
.unwrap_or(false);
|
||||||
|
let context_lines = if use_context { 3_u32 } else { 0_u32 };
|
||||||
|
super::list_remote_commit_files(
|
||||||
|
&project_repository.git_repository,
|
||||||
|
commit_oid,
|
||||||
|
context_lines,
|
||||||
|
)
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ pub struct RemoteBranchFile {
|
|||||||
pub fn list_remote_commit_files(
|
pub fn list_remote_commit_files(
|
||||||
repository: &git::Repository,
|
repository: &git::Repository,
|
||||||
commit_oid: git::Oid,
|
commit_oid: git::Oid,
|
||||||
|
context_lines: u32,
|
||||||
) -> Result<Vec<RemoteBranchFile>, errors::ListRemoteCommitFilesError> {
|
) -> Result<Vec<RemoteBranchFile>, errors::ListRemoteCommitFilesError> {
|
||||||
let commit = match repository.find_commit(commit_oid) {
|
let commit = match repository.find_commit(commit_oid) {
|
||||||
Ok(commit) => Ok(commit),
|
Ok(commit) => Ok(commit),
|
||||||
@ -35,7 +36,7 @@ pub fn list_remote_commit_files(
|
|||||||
let parent = commit.parent(0).context("failed to get parent commit")?;
|
let parent = commit.parent(0).context("failed to get parent commit")?;
|
||||||
let commit_tree = commit.tree().context("failed to get commit tree")?;
|
let commit_tree = commit.tree().context("failed to get commit tree")?;
|
||||||
let parent_tree = parent.tree().context("failed to get parent tree")?;
|
let parent_tree = parent.tree().context("failed to get parent tree")?;
|
||||||
let diff = diff::trees(repository, &parent_tree, &commit_tree)?;
|
let diff = diff::trees(repository, &parent_tree, &commit_tree, context_lines)?;
|
||||||
|
|
||||||
let files = diff
|
let files = diff
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -81,7 +81,7 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
|
|||||||
"line0\nline1\nline2\nline3\nline4\n",
|
"line0\nline1\nline2\nline3\nline4\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches[0];
|
let branch = &branches[0];
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
assert_eq!(branch.commits.len(), 0);
|
assert_eq!(branch.commits.len(), 0);
|
||||||
@ -99,7 +99,7 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// status (no files)
|
// status (no files)
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches[0];
|
let branch = &branches[0];
|
||||||
assert_eq!(branch.files.len(), 0);
|
assert_eq!(branch.files.len(), 0);
|
||||||
assert_eq!(branch.commits.len(), 1);
|
assert_eq!(branch.commits.len(), 1);
|
||||||
@ -110,7 +110,7 @@ fn test_commit_on_branch_then_change_file_then_get_status() -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// should have just the last change now, the other line is committed
|
// should have just the last change now, the other line is committed
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches[0];
|
let branch = &branches[0];
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
assert_eq!(branch.commits.len(), 1);
|
assert_eq!(branch.commits.len(), 1);
|
||||||
@ -170,7 +170,7 @@ fn test_signed_commit() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository).unwrap();
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository).unwrap();
|
||||||
let commit_id = &branches[0].commits[0].id;
|
let commit_id = &branches[0].commits[0].id;
|
||||||
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
||||||
// check the raw_header contains the string "SSH SIGNATURE"
|
// check the raw_header contains the string "SSH SIGNATURE"
|
||||||
@ -235,7 +235,7 @@ fn test_track_binary_files() -> Result<()> {
|
|||||||
let mut file = fs::File::create(std::path::Path::new(&project.path).join("image.bin"))?;
|
let mut file = fs::File::create(std::path::Path::new(&project.path).join("image.bin"))?;
|
||||||
file.write_all(&image_data)?;
|
file.write_all(&image_data)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches[0];
|
let branch = &branches[0];
|
||||||
assert_eq!(branch.files.len(), 2);
|
assert_eq!(branch.files.len(), 2);
|
||||||
let img_file = &branch
|
let img_file = &branch
|
||||||
@ -262,7 +262,7 @@ fn test_track_binary_files() -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// status (no files)
|
// status (no files)
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository).unwrap();
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository).unwrap();
|
||||||
let commit_id = &branches[0].commits[0].id;
|
let commit_id = &branches[0].commits[0].id;
|
||||||
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
||||||
let tree = commit_obj.tree()?;
|
let tree = commit_obj.tree()?;
|
||||||
@ -291,7 +291,7 @@ fn test_track_binary_files() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository).unwrap();
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository).unwrap();
|
||||||
let commit_id = &branches[0].commits[0].id;
|
let commit_id = &branches[0].commits[0].id;
|
||||||
// get tree from commit_id
|
// get tree from commit_id
|
||||||
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
let commit_obj = project_repository.git_repository.find_commit(*commit_id)?;
|
||||||
@ -621,7 +621,7 @@ fn test_updated_ownership_should_bubble_up() -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status");
|
get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status");
|
||||||
let files = branch_reader.read(&branch1_id)?.ownership.files;
|
let files = branch_reader.read(&branch1_id)?.ownership.files;
|
||||||
assert_eq!(files, vec!["test.txt:14-15,1-2".parse()?]);
|
assert_eq!(files, vec!["test.txt:11-15,1-5".parse()?]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
files[0].hunks[0].timestam_ms(),
|
files[0].hunks[0].timestam_ms(),
|
||||||
files[0].hunks[1].timestam_ms(),
|
files[0].hunks[1].timestam_ms(),
|
||||||
@ -641,7 +641,7 @@ fn test_updated_ownership_should_bubble_up() -> Result<()> {
|
|||||||
let files1 = branch_reader.read(&branch1_id)?.ownership.files;
|
let files1 = branch_reader.read(&branch1_id)?.ownership.files;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
files1,
|
files1,
|
||||||
vec!["test2.txt:1-2".parse()?, "test.txt:14-15,1-2".parse()?]
|
vec!["test2.txt:1-2".parse()?, "test.txt:11-15,1-5".parse()?]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
@ -667,7 +667,7 @@ fn test_updated_ownership_should_bubble_up() -> Result<()> {
|
|||||||
let files2 = branch_reader.read(&branch1_id)?.ownership.files;
|
let files2 = branch_reader.read(&branch1_id)?.ownership.files;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
files2,
|
files2,
|
||||||
vec!["test.txt:1-3,14-15".parse()?, "test2.txt:1-2".parse()?,]
|
vec!["test.txt:1-6,11-15".parse()?, "test2.txt:1-2".parse()?,]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
@ -736,12 +736,12 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
|
|||||||
let branch_writer = branch::Writer::new(&gb_repository)?;
|
let branch_writer = branch::Writer::new(&gb_repository)?;
|
||||||
let mut branch2 = branch_reader.read(&branch2_id)?;
|
let mut branch2 = branch_reader.read(&branch2_id)?;
|
||||||
branch2.ownership = Ownership {
|
branch2.ownership = Ownership {
|
||||||
files: vec!["test.txt:1-2".parse()?],
|
files: vec!["test.txt:1-5".parse()?],
|
||||||
};
|
};
|
||||||
branch_writer.write(&mut branch2)?;
|
branch_writer.write(&mut branch2)?;
|
||||||
let mut branch1 = branch_reader.read(&branch1_id)?;
|
let mut branch1 = branch_reader.read(&branch1_id)?;
|
||||||
branch1.ownership = Ownership {
|
branch1.ownership = Ownership {
|
||||||
files: vec!["test.txt:14-15".parse()?],
|
files: vec!["test.txt:11-15".parse()?],
|
||||||
};
|
};
|
||||||
branch_writer.write(&mut branch1)?;
|
branch_writer.write(&mut branch1)?;
|
||||||
|
|
||||||
@ -765,7 +765,7 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
|
|||||||
&project_repository,
|
&project_repository,
|
||||||
branch::BranchUpdateRequest {
|
branch::BranchUpdateRequest {
|
||||||
id: branch3_id,
|
id: branch3_id,
|
||||||
ownership: Some("test.txt:1-2,14-15".parse()?),
|
ownership: Some("test.txt:1-5,11-15".parse()?),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
@ -788,11 +788,11 @@ fn test_move_hunks_multiple_sources() -> Result<()> {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][0].diff,
|
files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][0].diff,
|
||||||
"@@ -0,0 +1 @@\n+line0\n"
|
"@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][1].diff,
|
files_by_branch_id[&branch3_id][std::path::Path::new("test.txt")][1].diff,
|
||||||
"@@ -12,0 +14 @@ line12\n+line13\n"
|
"@@ -10,3 +11,4 @@ line9\n line10\n line11\n line12\n+line13\n"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -848,7 +848,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
|
|||||||
&project_repository,
|
&project_repository,
|
||||||
branch::BranchUpdateRequest {
|
branch::BranchUpdateRequest {
|
||||||
id: branch2_id,
|
id: branch2_id,
|
||||||
ownership: Some("test.txt:1-2".parse()?),
|
ownership: Some("test.txt:1-5".parse()?),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
@ -869,7 +869,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
files_by_branch_id[&branch1_id][std::path::Path::new("test.txt")][0].diff,
|
files_by_branch_id[&branch1_id][std::path::Path::new("test.txt")][0].diff,
|
||||||
"@@ -13,0 +15 @@ line13\n+line14\n"
|
"@@ -11,3 +12,4 @@ line10\n line11\n line12\n line13\n+line14\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(files_by_branch_id[&branch2_id].len(), 1);
|
assert_eq!(files_by_branch_id[&branch2_id].len(), 1);
|
||||||
@ -879,7 +879,7 @@ fn test_move_hunks_partial_explicitly() -> Result<()> {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
files_by_branch_id[&branch2_id][std::path::Path::new("test.txt")][0].diff,
|
files_by_branch_id[&branch2_id][std::path::Path::new("test.txt")][0].diff,
|
||||||
"@@ -0,0 +1 @@\n+line0\n"
|
"@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -915,11 +915,7 @@ fn test_add_new_hunk_to_the_end() -> Result<()> {
|
|||||||
get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status");
|
get_status_by_branch(&gb_repository, &project_repository).expect("failed to get status");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
statuses[0].1[std::path::Path::new("test.txt")][0].diff,
|
statuses[0].1[std::path::Path::new("test.txt")][0].diff,
|
||||||
"@@ -14 +13,0 @@ line13\n-line13\n"
|
"@@ -11,5 +11,5 @@ line10\n line11\n line12\n line13\n-line13\n line14\n+line15\n"
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
statuses[0].1[std::path::Path::new("test.txt")][1].diff,
|
|
||||||
"@@ -15,0 +15 @@ line14\n+line15\n"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@ -932,15 +928,11 @@ fn test_add_new_hunk_to_the_end() -> Result<()> {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
statuses[0].1[std::path::Path::new("test.txt")][0].diff,
|
statuses[0].1[std::path::Path::new("test.txt")][0].diff,
|
||||||
"@@ -15,0 +16 @@ line14\n+line15\n"
|
"@@ -11,5 +12,5 @@ line10\n line11\n line12\n line13\n-line13\n line14\n+line15\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
statuses[0].1[std::path::Path::new("test.txt")][1].diff,
|
statuses[0].1[std::path::Path::new("test.txt")][1].diff,
|
||||||
"@@ -0,0 +1 @@\n+line0\n"
|
"@@ -1,3 +1,4 @@\n+line0\n line1\n line2\n line3\n"
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
statuses[0].1[std::path::Path::new("test.txt")][2].diff,
|
|
||||||
"@@ -14 +14,0 @@ line13\n-line13\n"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -1040,7 +1032,7 @@ fn test_merge_vbranch_upstream_clean_rebase() -> Result<()> {
|
|||||||
.context("failed to write target branch after push")?;
|
.context("failed to write target branch after push")?;
|
||||||
|
|
||||||
// create the branch
|
// create the branch
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches[0];
|
let branch1 = &branches[0];
|
||||||
assert_eq!(branch1.files.len(), 1);
|
assert_eq!(branch1.files.len(), 1);
|
||||||
assert_eq!(branch1.commits.len(), 1);
|
assert_eq!(branch1.commits.len(), 1);
|
||||||
@ -1054,7 +1046,7 @@ fn test_merge_vbranch_upstream_clean_rebase() -> Result<()> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches[0];
|
let branch1 = &branches[0];
|
||||||
|
|
||||||
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;
|
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;
|
||||||
@ -1163,7 +1155,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
|
|||||||
.context("failed to write target branch after push")?;
|
.context("failed to write target branch after push")?;
|
||||||
|
|
||||||
// create the branch
|
// create the branch
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches[0];
|
let branch1 = &branches[0];
|
||||||
|
|
||||||
assert_eq!(branch1.files.len(), 1);
|
assert_eq!(branch1.files.len(), 1);
|
||||||
@ -1172,7 +1164,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
|
|||||||
|
|
||||||
merge_virtual_branch_upstream(&gb_repository, &project_repository, &branch1.id, None, None)?;
|
merge_virtual_branch_upstream(&gb_repository, &project_repository, &branch1.id, None, None)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches[0];
|
let branch1 = &branches[0];
|
||||||
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;
|
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path))?;
|
||||||
|
|
||||||
@ -1192,7 +1184,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
// make gb see the conflict resolution
|
// make gb see the conflict resolution
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
assert!(branches[0].conflicted);
|
assert!(branches[0].conflicted);
|
||||||
|
|
||||||
// commit the merge resolution
|
// commit the merge resolution
|
||||||
@ -1207,7 +1199,7 @@ fn test_merge_vbranch_upstream_conflict() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches[0];
|
let branch1 = &branches[0];
|
||||||
assert!(!branch1.conflicted);
|
assert!(!branch1.conflicted);
|
||||||
assert_eq!(branch1.files.len(), 0);
|
assert_eq!(branch1.files.len(), 0);
|
||||||
@ -1247,7 +1239,7 @@ fn test_unapply_ownership_partial() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.expect("failed to create virtual branch");
|
.expect("failed to create virtual branch");
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
assert_eq!(branches.len(), 1);
|
assert_eq!(branches.len(), 1);
|
||||||
assert_eq!(branches[0].files.len(), 1);
|
assert_eq!(branches[0].files.len(), 1);
|
||||||
assert_eq!(branches[0].ownership.files.len(), 1);
|
assert_eq!(branches[0].ownership.files.len(), 1);
|
||||||
@ -1261,11 +1253,11 @@ fn test_unapply_ownership_partial() -> Result<()> {
|
|||||||
unapply_ownership(
|
unapply_ownership(
|
||||||
&gb_repository,
|
&gb_repository,
|
||||||
&project_repository,
|
&project_repository,
|
||||||
&"test.txt:5-6".parse().unwrap(),
|
&"test.txt:2-6".parse().unwrap(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
assert_eq!(branches.len(), 1);
|
assert_eq!(branches.len(), 1);
|
||||||
assert_eq!(branches[0].files.len(), 0);
|
assert_eq!(branches[0].files.len(), 0);
|
||||||
assert_eq!(branches[0].ownership.files.len(), 0);
|
assert_eq!(branches[0].ownership.files.len(), 0);
|
||||||
@ -1339,7 +1331,7 @@ fn test_apply_unapply_branch() -> Result<()> {
|
|||||||
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
|
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
|
||||||
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
|
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
assert!(branch.active);
|
assert!(branch.active);
|
||||||
@ -1351,7 +1343,7 @@ fn test_apply_unapply_branch() -> Result<()> {
|
|||||||
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
|
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
|
||||||
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
|
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
assert!(!branch.active);
|
assert!(!branch.active);
|
||||||
@ -1365,7 +1357,7 @@ fn test_apply_unapply_branch() -> Result<()> {
|
|||||||
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
|
let contents = std::fs::read(std::path::Path::new(&project.path).join(file_path2))?;
|
||||||
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
|
assert_eq!("line5\nline6\n", String::from_utf8(contents)?);
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
assert!(branch.active);
|
assert!(branch.active);
|
||||||
@ -1621,7 +1613,7 @@ fn test_detect_mergeable_branch() -> Result<()> {
|
|||||||
};
|
};
|
||||||
branch_writer.write(&mut branch4)?;
|
branch_writer.write(&mut branch4)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
assert_eq!(branches.len(), 4);
|
assert_eq!(branches.len(), 4);
|
||||||
|
|
||||||
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
@ -1803,7 +1795,7 @@ fn test_upstream_integrated_vbranch() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
|
|
||||||
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
assert!(branch1.commits.iter().any(|c| c.is_integrated));
|
assert!(branch1.commits.iter().any(|c| c.is_integrated));
|
||||||
@ -1850,7 +1842,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
|
|||||||
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
|
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
@ -1869,7 +1861,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 0, "no files expected");
|
assert_eq!(branch.files.len(), 0, "no files expected");
|
||||||
@ -1889,7 +1881,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
|
|||||||
"line1\nPATCH1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
|
"line1\nPATCH1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 1, "one file should be changed");
|
assert_eq!(branch.files.len(), 1, "one file should be changed");
|
||||||
@ -1906,7 +1898,7 @@ fn test_commit_same_hunk_twice() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -1951,7 +1943,7 @@ fn test_commit_same_file_twice() -> Result<()> {
|
|||||||
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
|
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\nline11\nline12\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
@ -1970,7 +1962,7 @@ fn test_commit_same_file_twice() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 0, "no files expected");
|
assert_eq!(branch.files.len(), 0, "no files expected");
|
||||||
@ -1990,7 +1982,7 @@ fn test_commit_same_file_twice() -> Result<()> {
|
|||||||
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
|
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 1, "one file should be changed");
|
assert_eq!(branch.files.len(), 1, "one file should be changed");
|
||||||
@ -2007,7 +1999,7 @@ fn test_commit_same_file_twice() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -2052,7 +2044,7 @@ fn test_commit_partial_by_hunk() -> Result<()> {
|
|||||||
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
|
"line1\npatch1\nline2\nline3\nline4\nline5\nmiddle\nmiddle\nmiddle\nmiddle\nline6\nline7\nline8\nline9\nline10\nmiddle\nmiddle\nmiddle\npatch2\nline11\nline12\n",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
@ -2065,13 +2057,13 @@ fn test_commit_partial_by_hunk() -> Result<()> {
|
|||||||
&project_repository,
|
&project_repository,
|
||||||
&branch1_id,
|
&branch1_id,
|
||||||
"first commit to test.txt",
|
"first commit to test.txt",
|
||||||
Some(&"test.txt:2-3".parse::<Ownership>().unwrap()),
|
Some(&"test.txt:1-6".parse::<Ownership>().unwrap()),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 1);
|
assert_eq!(branch.files.len(), 1);
|
||||||
@ -2085,13 +2077,13 @@ fn test_commit_partial_by_hunk() -> Result<()> {
|
|||||||
&project_repository,
|
&project_repository,
|
||||||
&branch1_id,
|
&branch1_id,
|
||||||
"second commit to test.txt",
|
"second commit to test.txt",
|
||||||
Some(&"test.txt:19-20".parse::<Ownership>().unwrap()),
|
Some(&"test.txt:16-22".parse::<Ownership>().unwrap()),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
assert_eq!(branch.files.len(), 0);
|
assert_eq!(branch.files.len(), 0);
|
||||||
@ -2158,7 +2150,7 @@ fn test_commit_partial_by_file() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
// branch one test.txt has just the 1st and 3rd hunks applied
|
// branch one test.txt has just the 1st and 3rd hunks applied
|
||||||
@ -2234,7 +2226,7 @@ fn test_commit_add_and_delete_files() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
// branch one test.txt has just the 1st and 3rd hunks applied
|
// branch one test.txt has just the 1st and 3rd hunks applied
|
||||||
@ -2305,7 +2297,7 @@ fn test_commit_executable_and_symlinks() -> Result<()> {
|
|||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
let branch1 = &branches.iter().find(|b| b.id == branch1_id).unwrap();
|
||||||
|
|
||||||
let commit = &branch1.commits[0].id;
|
let commit = &branch1.commits[0].id;
|
||||||
@ -2407,7 +2399,7 @@ fn test_verify_branch_commits_to_integration() -> Result<()> {
|
|||||||
integration::verify_branch(&gb_repository, &project_repository).unwrap();
|
integration::verify_branch(&gb_repository, &project_repository).unwrap();
|
||||||
|
|
||||||
// one virtual branch with two commits was created
|
// one virtual branch with two commits was created
|
||||||
let virtual_branches = list_virtual_branches(&gb_repository, &project_repository)?;
|
let (virtual_branches, _) = list_virtual_branches(&gb_repository, &project_repository)?;
|
||||||
assert_eq!(virtual_branches.len(), 1);
|
assert_eq!(virtual_branches.len(), 1);
|
||||||
|
|
||||||
let branch = &virtual_branches.first().unwrap();
|
let branch = &virtual_branches.first().unwrap();
|
||||||
|
@ -4,8 +4,9 @@ use std::{collections::HashMap, path, time, vec};
|
|||||||
use std::os::unix::prelude::*;
|
use std::os::unix::prelude::*;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use diffy::{apply_bytes, Patch};
|
use bstr::ByteSlice;
|
||||||
use git2_hooks::{HookResult, PrepareCommitMsgSource};
|
use diffy::{apply, Patch};
|
||||||
|
use git2_hooks::HookResult;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@ -728,7 +729,7 @@ fn find_base_tree<'a>(
|
|||||||
pub fn list_virtual_branches(
|
pub fn list_virtual_branches(
|
||||||
gb_repository: &gb_repository::Repository,
|
gb_repository: &gb_repository::Repository,
|
||||||
project_repository: &project_repository::Repository,
|
project_repository: &project_repository::Repository,
|
||||||
) -> Result<Vec<VirtualBranch>, errors::ListVirtualBranchesError> {
|
) -> Result<(Vec<VirtualBranch>, bool), errors::ListVirtualBranchesError> {
|
||||||
let mut branches: Vec<VirtualBranch> = Vec::new();
|
let mut branches: Vec<VirtualBranch> = Vec::new();
|
||||||
|
|
||||||
let default_target = gb_repository
|
let default_target = gb_repository
|
||||||
@ -889,6 +890,9 @@ pub fn list_virtual_branches(
|
|||||||
|
|
||||||
let branches = branches_with_large_files_abridged(branches);
|
let branches = branches_with_large_files_abridged(branches);
|
||||||
let mut branches = branches_with_hunk_locks(branches, project_repository)?;
|
let mut branches = branches_with_hunk_locks(branches, project_repository)?;
|
||||||
|
|
||||||
|
// If there no context lines are used internally, add them here, before returning to the UI
|
||||||
|
if context_lines(project_repository) == 0 {
|
||||||
for branch in &mut branches {
|
for branch in &mut branches {
|
||||||
branch.files = files_with_hunk_context(
|
branch.files = files_with_hunk_context(
|
||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
@ -898,12 +902,17 @@ pub fn list_virtual_branches(
|
|||||||
)
|
)
|
||||||
.context("failed to add hunk context")?;
|
.context("failed to add hunk context")?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
branches.sort_by(|a, b| a.order.cmp(&b.order));
|
branches.sort_by(|a, b| a.order.cmp(&b.order));
|
||||||
|
|
||||||
super::integration::update_gitbutler_integration(gb_repository, project_repository)?;
|
super::integration::update_gitbutler_integration(gb_repository, project_repository)?;
|
||||||
|
|
||||||
Ok(branches)
|
let uses_diff_context = project_repository
|
||||||
|
.project()
|
||||||
|
.use_diff_context
|
||||||
|
.unwrap_or(false);
|
||||||
|
Ok((branches, uses_diff_context))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn branches_with_large_files_abridged(mut branches: Vec<VirtualBranch>) -> Vec<VirtualBranch> {
|
fn branches_with_large_files_abridged(mut branches: Vec<VirtualBranch>) -> Vec<VirtualBranch> {
|
||||||
@ -940,6 +949,7 @@ fn branches_with_hunk_locks(
|
|||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
&parent_tree,
|
&parent_tree,
|
||||||
&commit_tree,
|
&commit_tree,
|
||||||
|
context_lines(project_repository),
|
||||||
)?;
|
)?;
|
||||||
for branch in &mut branches {
|
for branch in &mut branches {
|
||||||
for file in &mut branch.files {
|
for file in &mut branch.files {
|
||||||
@ -1091,6 +1101,7 @@ pub fn calculate_non_commited_diffs(
|
|||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
&branch_head,
|
&branch_head,
|
||||||
&branch_tree,
|
&branch_tree,
|
||||||
|
context_lines(project_repository),
|
||||||
)
|
)
|
||||||
.context("failed to diff trees")?;
|
.context("failed to diff trees")?;
|
||||||
|
|
||||||
@ -1132,6 +1143,7 @@ fn list_virtual_commit_files(
|
|||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
&parent_tree,
|
&parent_tree,
|
||||||
&commit_tree,
|
&commit_tree,
|
||||||
|
context_lines(project_repository),
|
||||||
)?;
|
)?;
|
||||||
let hunks_by_filepath = virtual_hunks_by_filepath(&project_repository.project().path, &diff);
|
let hunks_by_filepath = virtual_hunks_by_filepath(&project_repository.project().path, &diff);
|
||||||
Ok(virtual_hunks_to_virtual_files(
|
Ok(virtual_hunks_to_virtual_files(
|
||||||
@ -1895,6 +1907,7 @@ fn get_non_applied_status(
|
|||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
&target_tree,
|
&target_tree,
|
||||||
&branch_tree,
|
&branch_tree,
|
||||||
|
context_lines(project_repository),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok((branch, diff))
|
Ok((branch, diff))
|
||||||
@ -1913,7 +1926,11 @@ fn get_applied_status(
|
|||||||
default_target: &target::Target,
|
default_target: &target::Target,
|
||||||
mut virtual_branches: Vec<branch::Branch>,
|
mut virtual_branches: Vec<branch::Branch>,
|
||||||
) -> Result<AppliedStatuses> {
|
) -> Result<AppliedStatuses> {
|
||||||
let mut diff = diff::workdir(&project_repository.git_repository, &default_target.sha)
|
let mut diff = diff::workdir(
|
||||||
|
&project_repository.git_repository,
|
||||||
|
&default_target.sha,
|
||||||
|
context_lines(project_repository),
|
||||||
|
)
|
||||||
.context("failed to diff workdir")?;
|
.context("failed to diff workdir")?;
|
||||||
|
|
||||||
// sort by order, so that the default branch is first (left in the ui)
|
// sort by order, so that the default branch is first (left in the ui)
|
||||||
@ -2291,39 +2308,37 @@ pub fn write_tree_onto_tree(
|
|||||||
.peel_to_blob()
|
.peel_to_blob()
|
||||||
.context("failed to get blob")?;
|
.context("failed to get blob")?;
|
||||||
|
|
||||||
// get the contents
|
let mut blob_contents = blob.content().to_str()?.to_string();
|
||||||
let mut blob_contents = blob.content().to_vec();
|
|
||||||
|
|
||||||
let mut hunks = hunks.clone();
|
let mut hunks = hunks.clone();
|
||||||
hunks.sort_by_key(|hunk| hunk.new_start);
|
hunks.sort_by_key(|hunk| hunk.new_start);
|
||||||
|
let mut all_diffs = String::new();
|
||||||
for hunk in hunks {
|
for hunk in hunks {
|
||||||
let patch = format!("--- original\n+++ modified\n{}", hunk.diff);
|
all_diffs.push_str(&hunk.diff);
|
||||||
let patch_bytes = patch.as_bytes();
|
|
||||||
let patch = Patch::from_bytes(patch_bytes)?;
|
|
||||||
blob_contents = apply_bytes(&blob_contents, &patch)
|
|
||||||
.context(format!("failed to apply {}", &hunk.diff))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let patch = Patch::from_str(&all_diffs)?;
|
||||||
|
blob_contents = apply(&blob_contents, &patch)
|
||||||
|
.context(format!("failed to apply {}", &all_diffs))?;
|
||||||
|
|
||||||
// create a blob
|
// create a blob
|
||||||
let new_blob_oid = git_repository.blob(&blob_contents)?;
|
let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?;
|
||||||
// upsert into the builder
|
// upsert into the builder
|
||||||
builder.upsert(rel_path, new_blob_oid, filemode);
|
builder.upsert(rel_path, new_blob_oid, filemode);
|
||||||
}
|
}
|
||||||
} else if is_submodule {
|
} else if is_submodule {
|
||||||
let mut blob_contents = vec![];
|
let mut blob_contents = String::new();
|
||||||
|
|
||||||
let mut hunks = hunks.clone();
|
let mut hunks = hunks.clone();
|
||||||
hunks.sort_by_key(|hunk| hunk.new_start);
|
hunks.sort_by_key(|hunk| hunk.new_start);
|
||||||
for hunk in hunks {
|
for hunk in hunks {
|
||||||
let patch = format!("--- original\n+++ modified\n{}", hunk.diff);
|
let patch = Patch::from_str(&hunk.diff)?;
|
||||||
let patch_bytes = patch.as_bytes();
|
blob_contents = apply(&blob_contents, &patch)
|
||||||
let patch = Patch::from_bytes(patch_bytes)?;
|
|
||||||
blob_contents = apply_bytes(&blob_contents, &patch)
|
|
||||||
.context(format!("failed to apply {}", &hunk.diff))?;
|
.context(format!("failed to apply {}", &hunk.diff))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a blob
|
// create a blob
|
||||||
let new_blob_oid = git_repository.blob(&blob_contents)?;
|
let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?;
|
||||||
// upsert into the builder
|
// upsert into the builder
|
||||||
builder.upsert(rel_path, new_blob_oid, filemode);
|
builder.upsert(rel_path, new_blob_oid, filemode);
|
||||||
} else {
|
} else {
|
||||||
@ -3636,6 +3651,7 @@ pub fn move_commit(
|
|||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
&source_branch_head_parent_tree,
|
&source_branch_head_parent_tree,
|
||||||
&source_branch_head_tree,
|
&source_branch_head_tree,
|
||||||
|
context_lines(project_repository),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let is_source_locked = source_branch_non_comitted_files
|
let is_source_locked = source_branch_non_comitted_files
|
||||||
@ -3836,6 +3852,7 @@ pub fn create_virtual_branch_from_branch(
|
|||||||
&project_repository.git_repository,
|
&project_repository.git_repository,
|
||||||
&merge_base_tree,
|
&merge_base_tree,
|
||||||
&head_commit_tree,
|
&head_commit_tree,
|
||||||
|
context_lines(project_repository),
|
||||||
)
|
)
|
||||||
.context("failed to diff trees")?;
|
.context("failed to diff trees")?;
|
||||||
|
|
||||||
@ -3899,6 +3916,19 @@ pub fn create_virtual_branch_from_branch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn context_lines(project_repository: &project_repository::Repository) -> u32 {
|
||||||
|
let use_context = project_repository
|
||||||
|
.project()
|
||||||
|
.use_diff_context
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if use_context {
|
||||||
|
3_u32
|
||||||
|
} else {
|
||||||
|
0_u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -79,7 +79,7 @@ impl InnerHandler {
|
|||||||
.list_virtual_branches(project_id)
|
.list_virtual_branches(project_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(branches) => Ok(vec![events::Event::Emit(
|
Ok((branches, _)) => Ok(vec![events::Event::Emit(
|
||||||
app_events::Event::virtual_branches(
|
app_events::Event::virtual_branches(
|
||||||
project_id,
|
project_id,
|
||||||
&self.assets_proxy.proxy_virtual_branches(branches).await,
|
&self.assets_proxy.proxy_virtual_branches(branches).await,
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"dialog": true,
|
"dialog": false,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://app.gitbutler.com/releases/nightly/{{target}}-{{arch}}/{{current_version}}"
|
"https://app.gitbutler.com/releases/nightly/{{target}}-{{arch}}/{{current_version}}"
|
||||||
],
|
],
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"dialog": true,
|
"dialog": false,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://app.gitbutler.com/releases/release/{{target}}-{{arch}}/{{current_version}}"
|
"https://app.gitbutler.com/releases/release/{{target}}-{{arch}}/{{current_version}}"
|
||||||
],
|
],
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ export type Project = {
|
|||||||
preferred_key: Key;
|
preferred_key: Key;
|
||||||
ok_with_force_push: boolean;
|
ok_with_force_push: boolean;
|
||||||
omit_certificate_check: boolean | undefined;
|
omit_certificate_check: boolean | undefined;
|
||||||
|
use_diff_context: boolean | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ProjectService {
|
export class ProjectService {
|
||||||
|
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 { GitHubService } from '$lib/github/service';
|
||||||
import type { PullRequest } from '$lib/github/types';
|
import type { PullRequest } from '$lib/github/types';
|
||||||
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||||
|
import type { BranchController } from '$lib/vbranches/branchController';
|
||||||
import type { VirtualBranchService } from '$lib/vbranches/branchStoresCache';
|
import type { VirtualBranchService } from '$lib/vbranches/branchStoresCache';
|
||||||
import type { Branch, RemoteBranch } from '$lib/vbranches/types';
|
import type { Branch, RemoteBranch } from '$lib/vbranches/types';
|
||||||
|
|
||||||
@ -14,7 +15,8 @@ export class BranchService {
|
|||||||
constructor(
|
constructor(
|
||||||
private vbranchService: VirtualBranchService,
|
private vbranchService: VirtualBranchService,
|
||||||
remoteBranchService: RemoteBranchService,
|
remoteBranchService: RemoteBranchService,
|
||||||
private githubService: GitHubService
|
private githubService: GitHubService,
|
||||||
|
private branchController: BranchController
|
||||||
) {
|
) {
|
||||||
const vbranchesWithEmpty$ = vbranchService.branches$.pipe(startWith([]));
|
const vbranchesWithEmpty$ = vbranchService.branches$.pipe(startWith([]));
|
||||||
const branchesWithEmpty$ = remoteBranchService.branches$.pipe(startWith([]));
|
const branchesWithEmpty$ = remoteBranchService.branches$.pipe(startWith([]));
|
||||||
@ -47,7 +49,7 @@ export class BranchService {
|
|||||||
|
|
||||||
// Push if local commits
|
// Push if local commits
|
||||||
if (branch.commits.some((c) => !c.isRemote)) {
|
if (branch.commits.some((c) => !c.isRemote)) {
|
||||||
newBranch = await this.vbranchService.pushBranch(branch.id, branch.requiresForce);
|
newBranch = await this.branchController.pushBranch(branch.id, branch.requiresForce);
|
||||||
} else {
|
} else {
|
||||||
newBranch = branch;
|
newBranch = branch;
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,8 @@
|
|||||||
color var(--transition-fast),
|
color var(--transition-fast),
|
||||||
filter var(--transition-fast);
|
filter var(--transition-fast);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&.pop {
|
&.pop {
|
||||||
color: var(--clr-theme-scale-pop-10);
|
color: var(--clr-theme-scale-pop-10);
|
||||||
background: color-mix(
|
background: color-mix(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts" async="true">
|
<script lang="ts" async="true">
|
||||||
|
import FullscreenLoading from './FullscreenLoading.svelte';
|
||||||
import NewBranchDropZone from './NewBranchDropZone.svelte';
|
import NewBranchDropZone from './NewBranchDropZone.svelte';
|
||||||
import BranchLane from '$lib/components/BranchLane.svelte';
|
import BranchLane from '$lib/components/BranchLane.svelte';
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
@ -39,7 +40,7 @@
|
|||||||
{#if branchesError}
|
{#if branchesError}
|
||||||
<div class="p-4" data-tauri-drag-region>Something went wrong...</div>
|
<div class="p-4" data-tauri-drag-region>Something went wrong...</div>
|
||||||
{:else if !branches}
|
{:else if !branches}
|
||||||
<div class="loading" data-tauri-drag-region><Icon name="spinner" /></div>
|
<FullscreenLoading />
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="board"
|
class="board"
|
||||||
@ -213,13 +214,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty board */
|
/* Empty board */
|
||||||
|
|
||||||
.empty-board {
|
.empty-board {
|
||||||
|
@ -69,15 +69,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.branch-files__header {
|
.branch-files__header {
|
||||||
padding-top: var(--space-12);
|
padding-top: var(--space-14);
|
||||||
padding-bottom: var(--space-12);
|
padding-bottom: var(--space-12);
|
||||||
padding-left: var(--space-20);
|
padding-left: var(--space-14);
|
||||||
padding-right: var(--space-12);
|
padding-right: var(--space-14);
|
||||||
}
|
}
|
||||||
.files-padding {
|
.files-padding {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: var(--space-12);
|
padding-bottom: var(--space-12);
|
||||||
padding-left: var(--space-12);
|
padding-left: var(--space-14);
|
||||||
padding-right: var(--space-12);
|
padding-right: var(--space-14);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -407,7 +407,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
background: var(--clr-theme-container-pale);
|
background: var(--clr-theme-container-pale);
|
||||||
padding: var(--space-12);
|
padding: var(--space-14);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -452,7 +452,8 @@
|
|||||||
|
|
||||||
.header__remote-branch {
|
.header__remote-branch {
|
||||||
color: var(--clr-theme-scale-ntrl-50);
|
color: var(--clr-theme-scale-ntrl-50);
|
||||||
padding-left: var(--space-4);
|
padding-left: var(--space-2);
|
||||||
|
padding-right: var(--space-2);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -139,7 +139,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--space-20);
|
height: var(--space-20);
|
||||||
background: var(--target-branch-background);
|
background: var(--target-branch-background);
|
||||||
/* background-color: red; */
|
|
||||||
}
|
}
|
||||||
.header__info {
|
.header__info {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -152,7 +151,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
background: var(--clr-theme-container-pale);
|
background: var(--clr-theme-container-pale);
|
||||||
padding: var(--space-12);
|
padding: var(--space-14);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -171,7 +170,8 @@
|
|||||||
|
|
||||||
.header__remote-branch {
|
.header__remote-branch {
|
||||||
color: var(--clr-theme-scale-ntrl-50);
|
color: var(--clr-theme-scale-ntrl-50);
|
||||||
padding-left: var(--space-4);
|
padding-left: var(--space-2);
|
||||||
|
padding-right: var(--space-2);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
|
import { pxToRem } from '$lib/utils/pxToRem';
|
||||||
import { tooltip } from '$lib/utils/tooltip';
|
import { tooltip } from '$lib/utils/tooltip';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type iconsJson from '$lib/icons/icons.json';
|
import type iconsJson from '$lib/icons/icons.json';
|
||||||
|
|
||||||
|
export let size: 'medium' | 'large' = 'medium';
|
||||||
export let icon: keyof typeof iconsJson | undefined = undefined;
|
export let icon: keyof typeof iconsJson | undefined = undefined;
|
||||||
export let iconAlign: 'left' | 'right' = 'right';
|
export let iconAlign: 'left' | 'right' = 'right';
|
||||||
export let color: ButtonColor = 'primary';
|
export let color: ButtonColor = 'primary';
|
||||||
@ -21,6 +23,7 @@
|
|||||||
export let grow = false;
|
export let grow = false;
|
||||||
export let align: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'auto' = 'auto';
|
export let align: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline' | 'auto' = 'auto';
|
||||||
export let help = '';
|
export let help = '';
|
||||||
|
export let width: number | undefined = undefined;
|
||||||
|
|
||||||
export let element: HTMLAnchorElement | HTMLButtonElement | HTMLElement | null = null;
|
export let element: HTMLAnchorElement | HTMLButtonElement | HTMLElement | null = null;
|
||||||
|
|
||||||
@ -37,6 +40,8 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class={`btn ${className}`}
|
class={`btn ${className}`}
|
||||||
|
class:medium={size == 'medium'}
|
||||||
|
class:large={size == 'large'}
|
||||||
class:error-outline={color == 'error' && kind == 'outlined'}
|
class:error-outline={color == 'error' && kind == 'outlined'}
|
||||||
class:primary-outline={color == 'primary' && kind == 'outlined'}
|
class:primary-outline={color == 'primary' && kind == 'outlined'}
|
||||||
class:warn-outline={color == 'warn' && kind == 'outlined'}
|
class:warn-outline={color == 'warn' && kind == 'outlined'}
|
||||||
@ -51,6 +56,7 @@
|
|||||||
class:grow
|
class:grow
|
||||||
class:not-clickable={notClickable}
|
class:not-clickable={notClickable}
|
||||||
style:align-self={align}
|
style:align-self={align}
|
||||||
|
style:width={width ? pxToRem(width) : undefined}
|
||||||
use:tooltip={help}
|
use:tooltip={help}
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
@ -84,7 +90,9 @@
|
|||||||
min-width: var(--size-btn-m);
|
min-width: var(--size-btn-m);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
transition: background-color var(--transition-fast);
|
transition: background-color var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
@ -99,6 +107,7 @@
|
|||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
}
|
}
|
||||||
&.not-clickable {
|
&.not-clickable {
|
||||||
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,4 +186,16 @@
|
|||||||
border: 1px solid color-mix(in srgb, var(--clr-theme-err-outline), var(--darken-mid));
|
border: 1px solid color-mix(in srgb, var(--clr-theme-err-outline), var(--darken-mid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SIZE MODIFIERS */
|
||||||
|
|
||||||
|
.btn.medium {
|
||||||
|
height: var(--size-btn-m);
|
||||||
|
padding: var(--space-4) var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.large {
|
||||||
|
height: var(--size-btn-l);
|
||||||
|
padding: var(--space-6) var(--space-8);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
class="checkbox"
|
class="checkbox"
|
||||||
class:small
|
class:small
|
||||||
{value}
|
{value}
|
||||||
|
id={name}
|
||||||
{name}
|
{name}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
@ -33,6 +34,7 @@
|
|||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.checkbox {
|
.checkbox {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
width: var(--space-16);
|
width: var(--space-16);
|
||||||
height: var(--space-16);
|
height: var(--space-16);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -3,19 +3,34 @@
|
|||||||
|
|
||||||
export let padding: string = 'var(--space-16)';
|
export let padding: string = 'var(--space-16)';
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
export let checked = false;
|
||||||
|
export let hasTopRadius = true;
|
||||||
|
export let hasBottomRadius = true;
|
||||||
|
export let hasBottomLine = true;
|
||||||
|
|
||||||
const SLOTS = $$props.$$slots;
|
const SLOTS = $$props.$$slots;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatchClick = createEventDispatcher<{
|
||||||
click: void;
|
click: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const dispatchChange = createEventDispatcher<{
|
||||||
|
change: boolean;
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="clickable-card"
|
class="clickable-card"
|
||||||
|
class:has-top-radius={hasTopRadius}
|
||||||
|
class:has-bottom-radius={hasBottomRadius}
|
||||||
|
class:has-bottom-line={hasBottomLine}
|
||||||
style="padding: {padding}"
|
style="padding: {padding}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dispatch('click');
|
dispatchClick('click');
|
||||||
|
|
||||||
|
dispatchChange('change', checked);
|
||||||
|
|
||||||
|
checked = !checked;
|
||||||
}}
|
}}
|
||||||
class:card-disabled={disabled}
|
class:card-disabled={disabled}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -43,8 +58,8 @@
|
|||||||
.clickable-card {
|
.clickable-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-16);
|
gap: var(--space-16);
|
||||||
border-radius: var(--radius-l);
|
border-left: 1px solid var(--clr-theme-container-outline-light);
|
||||||
border: 1px solid var(--clr-theme-container-outline-light);
|
border-right: 1px solid var(--clr-theme-container-outline-light);
|
||||||
background-color: var(--clr-theme-container-light);
|
background-color: var(--clr-theme-container-light);
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition-fast),
|
background-color var(--transition-fast),
|
||||||
@ -84,4 +99,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* MODIFIERS */
|
||||||
|
|
||||||
|
.has-top-radius {
|
||||||
|
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||||
|
border-top-left-radius: var(--radius-l);
|
||||||
|
border-top-right-radius: var(--radius-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-bottom-radius {
|
||||||
|
border-bottom-left-radius: var(--radius-l);
|
||||||
|
border-bottom-right-radius: var(--radius-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-bottom-line {
|
||||||
|
border-bottom: 1px solid var(--clr-theme-container-outline-light);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getCloudApiClient, type User } from '$lib/backend/cloud';
|
import { getCloudApiClient, type User } from '$lib/backend/cloud';
|
||||||
import Checkbox from '$lib/components/Checkbox.svelte';
|
import ClickableCard from '$lib/components/ClickableCard.svelte';
|
||||||
import Link from '$lib/components/Link.svelte';
|
import Link from '$lib/components/Link.svelte';
|
||||||
import Login from '$lib/components/Login.svelte';
|
import Spacer from '$lib/components/Spacer.svelte';
|
||||||
|
import Toggle from '$lib/components/Toggle.svelte';
|
||||||
|
import WelcomeSigninAction from '$lib/components/WelcomeSigninAction.svelte';
|
||||||
import { projectAiGenEnabled } from '$lib/config/config';
|
import { projectAiGenEnabled } from '$lib/config/config';
|
||||||
import { projectAiGenAutoBranchNamingEnabled } from '$lib/config/config';
|
import { projectAiGenAutoBranchNamingEnabled } from '$lib/config/config';
|
||||||
import * as toasts from '$lib/utils/toasts';
|
import * as toasts from '$lib/utils/toasts';
|
||||||
@ -47,76 +49,58 @@
|
|||||||
toasts.error('Failed to update project sync status');
|
toasts.error('Failed to update project sync status');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="space-y-2">
|
const aiGenToggle = () => {
|
||||||
{#if user}
|
|
||||||
<h2 class="text-xl">GitButler Cloud</h2>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<span class="text-text-subdued"> Summary generation </span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex flex-row items-center justify-between rounded-lg border border-light-400 p-2 dark:border-dark-500"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col space-x-3">
|
|
||||||
<div class="flex flex-row items-center gap-x-1">
|
|
||||||
<Checkbox
|
|
||||||
name="sync"
|
|
||||||
disabled={user === undefined}
|
|
||||||
checked={$aiGenEnabled}
|
|
||||||
on:change={() => {
|
|
||||||
$aiGenEnabled = !$aiGenEnabled;
|
$aiGenEnabled = !$aiGenEnabled;
|
||||||
$aiGenAutoBranchNamingEnabled = $aiGenEnabled;
|
$aiGenAutoBranchNamingEnabled = $aiGenEnabled;
|
||||||
}}
|
};
|
||||||
/>
|
|
||||||
<label class="ml-2" for="sync">Enable branch and commit message generation.</label>
|
const aiGenBranchNamesToggle = () => {
|
||||||
</div>
|
$aiGenAutoBranchNamingEnabled = !$aiGenAutoBranchNamingEnabled;
|
||||||
<div class="pl-4 pr-8 text-sm text-light-700 dark:text-dark-200">
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if user}
|
||||||
|
<div class="aigen-wrap">
|
||||||
|
<ClickableCard on:click={aiGenToggle}>
|
||||||
|
<svelte:fragment slot="title">Enable branch and commit message generation</svelte:fragment>
|
||||||
|
<svelte:fragment slot="body">
|
||||||
Uses OpenAI's API. If enabled, diffs will sent to OpenAI's servers when pressing the
|
Uses OpenAI's API. If enabled, diffs will sent to OpenAI's servers when pressing the
|
||||||
"Generate message" button.
|
"Generate message" button.
|
||||||
</div>
|
</svelte:fragment>
|
||||||
<div class="flex flex-col space-x-3">
|
<svelte:fragment slot="actions">
|
||||||
<div class="flex flex-row items-center gap-x-1">
|
<Toggle checked={$aiGenEnabled} on:change={aiGenToggle} />
|
||||||
<Checkbox
|
</svelte:fragment>
|
||||||
name="sync"
|
</ClickableCard>
|
||||||
disabled={user === undefined || !$aiGenEnabled}
|
|
||||||
|
<ClickableCard disabled={!$aiGenEnabled} on:click={aiGenBranchNamesToggle}>
|
||||||
|
<svelte:fragment slot="title">Automatically generate branch names</svelte:fragment>
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<Toggle
|
||||||
|
disabled={!$aiGenEnabled}
|
||||||
checked={$aiGenAutoBranchNamingEnabled}
|
checked={$aiGenAutoBranchNamingEnabled}
|
||||||
on:change={() => {
|
on:change={aiGenBranchNamesToggle}
|
||||||
$aiGenAutoBranchNamingEnabled = !$aiGenAutoBranchNamingEnabled;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<label class="ml-2" for="sync">Automatically generate branch names.</label>
|
</svelte:fragment>
|
||||||
</div>
|
</ClickableCard>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if user.role === 'admin'}
|
|
||||||
<header>
|
|
||||||
<span class="text-text-subdued"> Full data synchronization </span>
|
|
||||||
</header>
|
|
||||||
<div
|
|
||||||
class="flex flex-row items-center justify-between rounded-lg border border-light-400 p-2 dark:border-dark-500"
|
|
||||||
>
|
|
||||||
<div class="flex flex-row space-x-3">
|
|
||||||
<div class="flex flex-row items-center gap-1">
|
|
||||||
<Checkbox
|
|
||||||
name="sync"
|
|
||||||
disabled={user === undefined}
|
|
||||||
checked={project.api?.sync || false}
|
|
||||||
on:change={onSyncChange}
|
|
||||||
/>
|
|
||||||
<label class="ml-2" for="sync">
|
|
||||||
Sync my history, repository and branch data for backup, sharing and team features.
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Spacer />
|
||||||
|
|
||||||
|
{#if user.role === 'admin'}
|
||||||
|
<h3 class="text-base-15 text-bold">Full data synchronization</h3>
|
||||||
|
|
||||||
|
<ClickableCard on:change={onSyncChange}>
|
||||||
|
<svelte:fragment slot="body">
|
||||||
|
Sync my history, repository and branch data for backup, sharing and team features.
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<Toggle checked={project.api?.sync || false} on:change={onSyncChange} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ClickableCard>
|
||||||
|
|
||||||
{#if project.api}
|
{#if project.api}
|
||||||
<div class="flex flex-row justify-end space-x-2">
|
<div class="api-link">
|
||||||
<div class="p-1">
|
|
||||||
<Link
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@ -124,10 +108,23 @@
|
|||||||
>Go to GitButler Cloud Project</Link
|
>Go to GitButler Cloud Project</Link
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<Spacer />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Login {userService} />
|
<WelcomeSigninAction {userService} />
|
||||||
{/if}
|
<Spacer />
|
||||||
</section>
|
{/if}
|
||||||
|
|
||||||
|
<style lang="post-css">
|
||||||
|
.aigen-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-link {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -154,7 +154,7 @@
|
|||||||
.commit {
|
.commit {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
cursor: default;
|
|
||||||
border-radius: var(--space-6);
|
border-radius: var(--space-6);
|
||||||
background-color: var(--clr-theme-container-light);
|
background-color: var(--clr-theme-container-light);
|
||||||
border: 1px solid var(--clr-theme-container-outline-light);
|
border: 1px solid var(--clr-theme-container-outline-light);
|
||||||
@ -173,10 +173,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.commit__header {
|
.commit__header {
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-10);
|
gap: var(--space-10);
|
||||||
padding: var(--space-12);
|
padding: var(--space-14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-commit-open {
|
.is-commit-open {
|
||||||
@ -263,7 +264,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--space-8);
|
gap: var(--space-8);
|
||||||
padding: var(--space-12);
|
padding: var(--space-14);
|
||||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -135,7 +135,7 @@
|
|||||||
on:focus={useAutoHeight}
|
on:focus={useAutoHeight}
|
||||||
on:change={() => currentCommitMessage.set(commitMessage)}
|
on:change={() => currentCommitMessage.set(commitMessage)}
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
class="text-input commit-box__textarea"
|
class="text-input text-base-body-13 commit-box__textarea"
|
||||||
rows="1"
|
rows="1"
|
||||||
disabled={isGeneratingCommigMessage}
|
disabled={isGeneratingCommigMessage}
|
||||||
placeholder="Your commit message here"
|
placeholder="Your commit message here"
|
||||||
@ -217,7 +217,7 @@
|
|||||||
.commit-box {
|
.commit-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: var(--space-16);
|
padding: var(--space-14);
|
||||||
background: var(--clr-theme-container-light);
|
background: var(--clr-theme-container-light);
|
||||||
border-top: 1px solid var(--clr-theme-container-outline-light);
|
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||||
transition: background-color var(--transition-medium);
|
transition: background-color var(--transition-medium);
|
||||||
|
@ -91,7 +91,7 @@
|
|||||||
.commit-list__content {
|
.commit-list__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 var(--space-16) var(--space-20) var(--space-16);
|
padding: 0 var(--space-14) var(--space-20) var(--space-14);
|
||||||
gap: var(--space-8);
|
gap: var(--space-8);
|
||||||
}
|
}
|
||||||
.upstream-message {
|
.upstream-message {
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--space-16) var(--space-12) var(--space-16) var(--space-16);
|
padding: var(--space-16) var(--space-14) var(--space-16) var(--space-14);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--space-8);
|
gap: var(--space-8);
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||||
|
import Spacer from '$lib/components/Spacer.svelte';
|
||||||
import TextArea from '$lib/components/TextArea.svelte';
|
import TextArea from '$lib/components/TextArea.svelte';
|
||||||
import TextBox from '$lib/components/TextBox.svelte';
|
import TextBox from '$lib/components/TextBox.svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
@ -14,15 +16,13 @@
|
|||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="flex flex-col gap-3">
|
<SectionCard>
|
||||||
<fieldset class="flex flex-col gap-3">
|
<form>
|
||||||
<div class="flex flex-col gap-1">
|
<fieldset class="fields-wrapper">
|
||||||
<label for="path">Path</label>
|
<TextBox label="Path" readonly id="path" value={project?.path} />
|
||||||
<TextBox readonly id="path" value={project?.path} />
|
<section class="description-wrapper">
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label for="name">Project Name</label>
|
|
||||||
<TextBox
|
<TextBox
|
||||||
|
label="Project Name"
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Project name can't be empty"
|
placeholder="Project name can't be empty"
|
||||||
bind:value={title}
|
bind:value={title}
|
||||||
@ -32,18 +32,32 @@
|
|||||||
dispatch('updated', project);
|
dispatch('updated', project);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label for="description">Project Description</label>
|
|
||||||
<TextArea
|
<TextArea
|
||||||
id="description"
|
id="description"
|
||||||
rows={3}
|
rows={3}
|
||||||
|
placeholder="Project description"
|
||||||
bind:value={description}
|
bind:value={description}
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
project.description = e.detail;
|
project.description = e.detail;
|
||||||
dispatch('updated', project);
|
dispatch('updated', project);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
</SectionCard>
|
||||||
|
<Spacer />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.fields-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Button from './Button.svelte';
|
||||||
import HunkViewer from './HunkViewer.svelte';
|
import HunkViewer from './HunkViewer.svelte';
|
||||||
import Icon from './Icon.svelte';
|
import Icon from './Icon.svelte';
|
||||||
import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
|
import { computeAddedRemovedByHunk } from '$lib/utils/metrics';
|
||||||
@ -30,6 +31,8 @@
|
|||||||
|
|
||||||
$: maxLineNumber = sections[sections.length - 1]?.maxLineNumber;
|
$: maxLineNumber = sections[sections.length - 1]?.maxLineNumber;
|
||||||
$: minWidth = getGutterMinWidth(maxLineNumber);
|
$: minWidth = getGutterMinWidth(maxLineNumber);
|
||||||
|
|
||||||
|
let alwaysShow = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="hunks">
|
<div class="hunks">
|
||||||
@ -37,6 +40,13 @@
|
|||||||
Binary content not shown
|
Binary content not shown
|
||||||
{:else if isLarge}
|
{:else if isLarge}
|
||||||
Diff too large to be shown
|
Diff too large to be shown
|
||||||
|
{:else if sections.length > 50 && !alwaysShow}
|
||||||
|
<div class="flex flex-col p-1">
|
||||||
|
Change hidden as large numbers of diffs may slow down the UI
|
||||||
|
<Button kind="outlined" color="neutral" on:click={() => (alwaysShow = true)}
|
||||||
|
>show anyways</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each sections as section}
|
{#each sections as section}
|
||||||
{@const { added, removed } = computeAddedRemovedByHunk(section)}
|
{@const { added, removed } = computeAddedRemovedByHunk(section)}
|
||||||
|
@ -50,7 +50,25 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="list-item-wrapper">
|
||||||
|
{#if showCheckbox}
|
||||||
|
<Checkbox
|
||||||
|
small
|
||||||
|
{checked}
|
||||||
|
{indeterminate}
|
||||||
|
on:change={(e) => {
|
||||||
|
selectedOwnership.update((ownership) => {
|
||||||
|
if (e.detail) file.hunks.forEach((h) => ownership.addHunk(file.id, h.id));
|
||||||
|
if (!e.detail) file.hunks.forEach((h) => ownership.removeHunk(file.id, h.id));
|
||||||
|
return ownership;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="file-list-item"
|
||||||
|
id={`file-${file.id}`}
|
||||||
|
class:selected-draggable={selected}
|
||||||
on:click
|
on:click
|
||||||
on:keydown
|
on:keydown
|
||||||
on:dragstart={() => {
|
on:dragstart={() => {
|
||||||
@ -70,29 +88,8 @@
|
|||||||
popupMenu.openByMouse(e, {
|
popupMenu.openByMouse(e, {
|
||||||
files: $selectedFiles.includes(file) ? $selectedFiles : [file]
|
files: $selectedFiles.includes(file) ? $selectedFiles : [file]
|
||||||
})}
|
})}
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="file-list-item"
|
|
||||||
id={`file-${file.id}`}
|
|
||||||
class:selected-draggable={selected}
|
|
||||||
role="listitem"
|
|
||||||
on:contextmenu|preventDefault
|
|
||||||
>
|
>
|
||||||
<div class="info-wrap">
|
<div class="info-wrap">
|
||||||
{#if showCheckbox}
|
|
||||||
<Checkbox
|
|
||||||
small
|
|
||||||
{checked}
|
|
||||||
{indeterminate}
|
|
||||||
on:change={(e) => {
|
|
||||||
selectedOwnership.update((ownership) => {
|
|
||||||
if (e.detail) file.hunks.forEach((h) => ownership.addHunk(file.id, h.id));
|
|
||||||
if (!e.detail) file.hunks.forEach((h) => ownership.removeHunk(file.id, h.id));
|
|
||||||
return ownership;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<img src={getVSIFileIcon(file.path)} alt="js" style="width: var(--space-12)" />
|
<img src={getVSIFileIcon(file.path)} alt="js" style="width: var(--space-12)" />
|
||||||
<span class="text-base-12 name">
|
<span class="text-base-12 name">
|
||||||
@ -108,7 +105,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
.list-item-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
.file-list-item {
|
.file-list-item {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: var(--space-28);
|
height: var(--space-28);
|
||||||
|
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">
|
<script lang="ts" context="module">
|
||||||
|
import { pxToRem } from '$lib/utils/pxToRem';
|
||||||
export type IconColor = 'success' | 'error' | 'pop' | 'warn' | 'neutral' | undefined;
|
export type IconColor = 'success' | 'error' | 'pop' | 'warn' | 'neutral' | undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -10,8 +11,6 @@
|
|||||||
export let opacity: number | undefined = 1;
|
export let opacity: number | undefined = 1;
|
||||||
export let spinnerRadius: number | undefined = 5;
|
export let spinnerRadius: number | undefined = 5;
|
||||||
export let size = 16;
|
export let size = 16;
|
||||||
|
|
||||||
const pxToRem = (px: number) => `${px / 16}rem`;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
|
@ -32,9 +32,9 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--clr-theme-scale-ntrl-40);
|
|
||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
color: var(--clr-theme-scale-ntrl-50);
|
color: var(--clr-theme-scale-ntrl-50);
|
||||||
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition-fast),
|
background-color var(--transition-fast),
|
||||||
color var(--transition-fast);
|
color var(--transition-fast);
|
||||||
@ -45,6 +45,7 @@
|
|||||||
}
|
}
|
||||||
.selected {
|
.selected {
|
||||||
background-color: color-mix(in srgb, transparent, var(--darken-tint-light));
|
background-color: color-mix(in srgb, transparent, var(--darken-tint-light));
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
.large {
|
.large {
|
||||||
height: var(--size-btn-l);
|
height: var(--size-btn-l);
|
||||||
|
@ -125,4 +125,8 @@
|
|||||||
:global(.info-message__text p:not(:last-child)) {
|
:global(.info-message__text p:not(:last-child)) {
|
||||||
margin-bottom: var(--space-10);
|
margin-bottom: var(--space-10);
|
||||||
}
|
}
|
||||||
|
:global(.info-message__text ul) {
|
||||||
|
list-style-type: circle;
|
||||||
|
padding: 0 0 0 var(--space-16);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ClickableCard from './ClickableCard.svelte';
|
||||||
|
import RadioButton from './RadioButton.svelte';
|
||||||
|
import SectionCard from './SectionCard.svelte';
|
||||||
|
import Spacer from './Spacer.svelte';
|
||||||
|
import TextBox from './TextBox.svelte';
|
||||||
import { invoke } from '$lib/backend/ipc';
|
import { invoke } from '$lib/backend/ipc';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import Link from '$lib/components/Link.svelte';
|
import Link from '$lib/components/Link.svelte';
|
||||||
import TextBox from '$lib/components/TextBox.svelte';
|
|
||||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||||
import { debounce } from '$lib/utils/debounce';
|
import { debounce } from '$lib/utils/debounce';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
@ -81,121 +85,199 @@
|
|||||||
preferred_key: 'generated'
|
preferred_key: 'generated'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let showPassphrase = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<section class="git-auth-wrap">
|
||||||
<p>Git Authentication</p>
|
<h3 class="text-base-15 text-bold">Git Authentication</h3>
|
||||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
<p class="text-base-body-12">
|
||||||
<div>
|
|
||||||
Configure the authentication flow for GitButler when authenticating with your Git remote
|
Configure the authentication flow for GitButler when authenticating with your Git remote
|
||||||
provider.
|
provider.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-2" style="grid-template-columns: max-content 1fr;">
|
<form>
|
||||||
<input type="radio" bind:group={selectedOption} value="default" on:input={setDefaultKey} />
|
<fieldset class="git-radio">
|
||||||
<div class="flex flex-col space-y-2">
|
<ClickableCard
|
||||||
<div>Auto detect</div>
|
hasBottomRadius={false}
|
||||||
{#if selectedOption === 'default'}
|
on:click={() => {
|
||||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
if (selectedOption == 'default') return;
|
||||||
<div>GitButler will attempt all available authentication flows automatically.</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="radio" bind:group={selectedOption} value="local" />
|
selectedOption = 'default';
|
||||||
<div class="flex flex-col space-y-2">
|
setDefaultKey();
|
||||||
<div>Use existing SSH key</div>
|
}}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="title">Auto detect</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<RadioButton bind:group={selectedOption} value="default" on:input={setDefaultKey} />
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="body">
|
||||||
|
GitButler will attempt all available authentication flows automatically.
|
||||||
|
</svelte:fragment>
|
||||||
|
</ClickableCard>
|
||||||
|
|
||||||
|
<ClickableCard
|
||||||
|
hasTopRadius={false}
|
||||||
|
hasBottomRadius={false}
|
||||||
|
hasBottomLine={selectedOption !== 'local'}
|
||||||
|
on:click={() => {
|
||||||
|
selectedOption = 'local';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="title">Use existing SSH key</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<RadioButton bind:group={selectedOption} value="local" />
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="body">
|
||||||
|
Add the path to an existing SSH key that GitButler can use.
|
||||||
|
</svelte:fragment>
|
||||||
|
</ClickableCard>
|
||||||
|
|
||||||
{#if selectedOption === 'local'}
|
{#if selectedOption === 'local'}
|
||||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
<SectionCard hasTopRadius={false} hasBottomRadius={false}>
|
||||||
Add the path to an existing SSH key that GitButler can use.
|
<div class="inputs-group">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-2 items-center gap-2"
|
|
||||||
style="grid-template-columns: max-content 1fr;"
|
|
||||||
>
|
|
||||||
<label for="path">Path to private key</label>
|
|
||||||
|
|
||||||
<TextBox
|
<TextBox
|
||||||
|
label="Path to private key"
|
||||||
placeholder="for example: ~/.ssh/id_rsa"
|
placeholder="for example: ~/.ssh/id_rsa"
|
||||||
bind:value={privateKeyPath}
|
bind:value={privateKeyPath}
|
||||||
on:input={debounce(setLocalKey, 600)}
|
on:input={debounce(setLocalKey, 600)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="passphrase">Passphrase (optional)</label>
|
<div class="input-with-button">
|
||||||
<TextBox
|
<TextBox
|
||||||
type="password"
|
label="Passphrase (optional)"
|
||||||
|
type={showPassphrase ? 'text' : 'password'}
|
||||||
bind:value={privateKeyPassphrase}
|
bind:value={privateKeyPassphrase}
|
||||||
on:input={debounce(setLocalKey, 600)}
|
on:input={debounce(setLocalKey, 600)}
|
||||||
|
wide
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
color="neutral"
|
||||||
|
kind="outlined"
|
||||||
|
icon={showPassphrase ? 'eye-shown' : 'eye-hidden'}
|
||||||
|
on:click={() => (showPassphrase = !showPassphrase)}
|
||||||
|
width={150}
|
||||||
|
>
|
||||||
|
{showPassphrase ? 'Hide passphrase' : 'Show passphrase'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionCard>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="radio" bind:group={selectedOption} value="generated" on:input={setGeneratedKey} />
|
<ClickableCard
|
||||||
<div class="flex flex-col space-y-2">
|
hasTopRadius={false}
|
||||||
<div class="pr-8">
|
hasBottomRadius={false}
|
||||||
<div>Use locally generated SSH key</div>
|
hasBottomLine={selectedOption !== 'generated'}
|
||||||
</div>
|
on:click={() => {
|
||||||
|
if (selectedOption == 'generated') return;
|
||||||
|
|
||||||
|
selectedOption = 'generated';
|
||||||
|
setGeneratedKey();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="title">Use locally generated SSH key</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<RadioButton bind:group={selectedOption} value="generated" />
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="body">
|
||||||
|
GitButler will use a locally generated SSH key. For this to work you need to add the
|
||||||
|
following public key to your Git remote provider:
|
||||||
|
</svelte:fragment>
|
||||||
|
</ClickableCard>
|
||||||
|
|
||||||
{#if selectedOption === 'generated'}
|
{#if selectedOption === 'generated'}
|
||||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
<SectionCard hasTopRadius={false} hasBottomRadius={false}>
|
||||||
GitButler will use a locally generated SSH key. For this to work you <b>need</b>
|
<TextBox readonly selectall bind:value={sshKey} />
|
||||||
to add the following public key to your Git remote provider:
|
<div class="row-buttons">
|
||||||
</div>
|
|
||||||
<div class="flex-auto overflow-y-scroll">
|
|
||||||
<input
|
|
||||||
bind:value={sshKey}
|
|
||||||
disabled={selectedOption !== 'generated'}
|
|
||||||
class="whitespece-pre input w-full select-all rounded border p-2 font-mono"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row justify-end space-x-2">
|
|
||||||
<div>
|
|
||||||
<Button
|
<Button
|
||||||
kind="filled"
|
kind="filled"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
icon="copy"
|
||||||
on:click={() => copyToClipboard(sshKey)}
|
on:click={() => copyToClipboard(sshKey)}
|
||||||
disabled={selectedOption !== 'generated'}
|
|
||||||
>
|
>
|
||||||
Copy to Clipboard
|
Copy to Clipboard
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
<div class="p-1">
|
kind="outlined"
|
||||||
<Link
|
color="neutral"
|
||||||
target="_blank"
|
icon="open-link"
|
||||||
rel="noreferrer"
|
on:click={() => {
|
||||||
href="https://github.com/settings/ssh/new"
|
open('https://github.com/settings/ssh/new');
|
||||||
disabled={selectedOption !== 'generated'}
|
}}
|
||||||
>
|
>
|
||||||
Add key to GitHub
|
Add key to GitHub
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</SectionCard>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
<ClickableCard
|
||||||
type="radio"
|
hasTopRadius={false}
|
||||||
bind:group={selectedOption}
|
on:click={() => {
|
||||||
value="gitCredentialsHelper"
|
if (selectedOption == 'gitCredentialsHelper') return;
|
||||||
on:input={setGitCredentialsHelperKey}
|
|
||||||
/>
|
selectedOption = 'gitCredentialsHelper';
|
||||||
<div class="flex flex-col space-y-2">
|
setGitCredentialsHelperKey();
|
||||||
<div class="pr-8">
|
}}
|
||||||
<div>
|
|
||||||
Use a
|
|
||||||
<Link target="_blank" rel="noreferrer" href="https://git-scm.com/doc/credential-helpers"
|
|
||||||
>Git credentials helper</Link
|
|
||||||
>
|
>
|
||||||
</div>
|
<svelte:fragment slot="title">Use a Git credentials helper</svelte:fragment>
|
||||||
</div>
|
|
||||||
{#if selectedOption === 'gitCredentialsHelper'}
|
<svelte:fragment slot="body">
|
||||||
<div class="pr-8 text-sm text-light-700 dark:text-dark-200">
|
|
||||||
GitButler will use the system's git credentials helper.
|
GitButler will use the system's git credentials helper.
|
||||||
</div>
|
<Link target="_blank" rel="noreferrer" href="https://git-scm.com/doc/credential-helpers">
|
||||||
{/if}
|
Learn more
|
||||||
</div>
|
</Link>
|
||||||
</div>
|
</svelte:fragment>
|
||||||
</div>
|
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<RadioButton bind:group={selectedOption} value="gitCredentialsHelper" />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ClickableCard>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Spacer />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.git-auth-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-16);
|
||||||
|
|
||||||
|
& p {
|
||||||
|
color: var(--clr-theme-scale-ntrl-30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-button {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-8);
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-radio {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Checkbox from '$lib/components/Checkbox.svelte';
|
import Spacer from './Spacer.svelte';
|
||||||
|
import ClickableCard from '$lib/components/ClickableCard.svelte';
|
||||||
|
import Toggle from '$lib/components/Toggle.svelte';
|
||||||
import { projectRunCommitHooks } from '$lib/config/config';
|
import { projectRunCommitHooks } from '$lib/config/config';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import type { Project } from '$lib/backend/projects';
|
import type { Project } from '$lib/backend/projects';
|
||||||
@ -16,57 +18,70 @@
|
|||||||
omit_certificate_check?: boolean;
|
omit_certificate_check?: boolean;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const onAllowForcePushingChange = () => {
|
||||||
|
dispatch('updated', { ok_with_force_push: allowForcePushing });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOmitCertificateCheckChange = () => {
|
||||||
|
dispatch('updated', { omit_certificate_check: omitCertificateCheck });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRunCommitHooksChange = () => {
|
||||||
|
$runCommitHooks = !$runCommitHooks;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-1 rounded-lg border border-light-400 p-2 dark:border-dark-500">
|
<section class="wrapper">
|
||||||
<form class="flex items-center gap-1">
|
<ClickableCard
|
||||||
<Checkbox
|
on:click={() => {
|
||||||
name="allow-force-pushing"
|
|
||||||
checked={allowForcePushing}
|
|
||||||
on:change={() => {
|
|
||||||
allowForcePushing = !allowForcePushing;
|
allowForcePushing = !allowForcePushing;
|
||||||
dispatch('updated', { ok_with_force_push: allowForcePushing });
|
onAllowForcePushingChange();
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<label class="ml-2" for="allow-force-pushing">
|
<svelte:fragment slot="title">Allow force pushing</svelte:fragment>
|
||||||
<div>Allow force pushing</div>
|
<svelte:fragment slot="body">
|
||||||
</label>
|
Force pushing allows GitButler to override branches even if they were pushed to remote. We
|
||||||
</form>
|
will never force push to the trunk.
|
||||||
<p class="ml-7 text-light-700 dark:text-dark-200">
|
</svelte:fragment>
|
||||||
Force pushing allows GitButler to override branches even if they were pushed to remote. We will
|
<svelte:fragment slot="actions">
|
||||||
never force push to the trunk.
|
<Toggle bind:checked={allowForcePushing} on:change={onAllowForcePushingChange} />
|
||||||
</p>
|
</svelte:fragment>
|
||||||
|
</ClickableCard>
|
||||||
|
|
||||||
<form class="flex items-center gap-1">
|
<ClickableCard
|
||||||
<Checkbox
|
on:click={() => {
|
||||||
name="omit-certificate-check"
|
|
||||||
checked={omitCertificateCheck}
|
|
||||||
on:change={() => {
|
|
||||||
omitCertificateCheck = !omitCertificateCheck;
|
omitCertificateCheck = !omitCertificateCheck;
|
||||||
dispatch('updated', { omit_certificate_check: omitCertificateCheck });
|
onOmitCertificateCheckChange();
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<label class="ml-2" for="allow-force-pushing">
|
<svelte:fragment slot="title">Ignore host certificate checks</svelte:fragment>
|
||||||
<div>Ignore host certificate checks</div>
|
<svelte:fragment slot="body">
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
<p class="ml-7 text-light-700 dark:text-dark-200">
|
|
||||||
Enabling this will ignore host certificate checks when authenticating with ssh.
|
Enabling this will ignore host certificate checks when authenticating with ssh.
|
||||||
</p>
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="actions">
|
||||||
|
<Toggle bind:checked={omitCertificateCheck} on:change={onOmitCertificateCheckChange} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ClickableCard>
|
||||||
|
|
||||||
<form class="flex items-center gap-1">
|
<ClickableCard on:click={onRunCommitHooksChange}>
|
||||||
<Checkbox
|
<svelte:fragment slot="title">Run commit hooks</svelte:fragment>
|
||||||
name="run-commit-hooks"
|
<svelte:fragment slot="body">
|
||||||
checked={$runCommitHooks}
|
Enabling this will run any git pre and post commit hooks you have configured in your
|
||||||
on:change={() => {
|
repository.
|
||||||
$runCommitHooks = !$runCommitHooks;
|
</svelte:fragment>
|
||||||
}}
|
<svelte:fragment slot="actions">
|
||||||
/>
|
<Toggle bind:checked={$runCommitHooks} on:change={onRunCommitHooksChange} />
|
||||||
<label class="ml-2" for="allow-force-pushing">
|
</svelte:fragment>
|
||||||
<div>Run commit hooks</div>
|
</ClickableCard>
|
||||||
</label>
|
</section>
|
||||||
</form>
|
|
||||||
<p class="ml-7 text-light-700 dark:text-dark-200">
|
<Spacer />
|
||||||
Enabling this will run any git pre and post commit hooks you have configured in your repository.
|
|
||||||
</p>
|
<style lang="post-css">
|
||||||
</div>
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -29,10 +29,14 @@
|
|||||||
(b) => b.name == 'origin/master' || b.name == 'origin/main'
|
(b) => b.name == 'origin/master' || b.name == 'origin/main'
|
||||||
);
|
);
|
||||||
|
|
||||||
function onSetTargetClick() {
|
async function onSetTargetClick() {
|
||||||
if (!selectedBranch) return;
|
if (!selectedBranch) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
branchController.setTarget(selectedBranch.name).finally(() => (loading = false));
|
try {
|
||||||
|
await branchController.setTarget(selectedBranch.name);
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.radio {
|
.radio {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
width: var(--space-16);
|
width: var(--space-16);
|
||||||
height: var(--space-16);
|
height: var(--space-16);
|
||||||
border-radius: var(--space-16);
|
border-radius: var(--space-16);
|
||||||
|
@ -25,10 +25,10 @@
|
|||||||
modal.show();
|
modal.show();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove project …
|
Remove project
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Modal bind:this={modal} title="Remove from GitButler">
|
<Modal bind:this={modal}>
|
||||||
<div class="remove-project-description">
|
<div class="remove-project-description">
|
||||||
<p class="text-base-body-14">
|
<p class="text-base-body-14">
|
||||||
Are you sure you want to remove
|
Are you sure you want to remove
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let orientation: 'row' | 'column' = 'column';
|
export let orientation: 'row' | 'column' = 'column';
|
||||||
|
export let hasTopRadius = true;
|
||||||
|
export let hasBottomRadius = true;
|
||||||
|
export let hasBottomLine = true;
|
||||||
|
|
||||||
const SLOTS = $$props.$$slots;
|
const SLOTS = $$props.$$slots;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="section-card" style:flex-direction={orientation}>
|
<section
|
||||||
|
class="section-card"
|
||||||
|
style:flex-direction={orientation}
|
||||||
|
class:has-top-radius={hasTopRadius}
|
||||||
|
class:has-bottom-radius={hasBottomRadius}
|
||||||
|
class:has-bottom-line={hasBottomLine}
|
||||||
|
>
|
||||||
{#if SLOTS.iconSide}
|
{#if SLOTS.iconSide}
|
||||||
<div class="section-card__icon-side">
|
<div class="section-card__icon-side">
|
||||||
<slot name="iconSide" />
|
<slot name="iconSide" />
|
||||||
@ -32,8 +42,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-16);
|
gap: var(--space-16);
|
||||||
padding: var(--space-16);
|
padding: var(--space-16);
|
||||||
border-radius: var(--radius-m);
|
border-left: 1px solid var(--clr-theme-container-outline-light);
|
||||||
border: 1px solid var(--clr-theme-container-outline-light);
|
border-right: 1px solid var(--clr-theme-container-outline-light);
|
||||||
background-color: var(--clr-theme-container-light);
|
background-color: var(--clr-theme-container-light);
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition-fast),
|
background-color var(--transition-fast),
|
||||||
@ -55,4 +65,21 @@
|
|||||||
.section-card__text {
|
.section-card__text {
|
||||||
color: var(--clr-theme-scale-ntrl-30);
|
color: var(--clr-theme-scale-ntrl-30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* MODIFIERS */
|
||||||
|
|
||||||
|
.has-top-radius {
|
||||||
|
border-top: 1px solid var(--clr-theme-container-outline-light);
|
||||||
|
border-top-left-radius: var(--radius-m);
|
||||||
|
border-top-right-radius: var(--radius-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-bottom-radius {
|
||||||
|
border-bottom-left-radius: var(--radius-m);
|
||||||
|
border-bottom-right-radius: var(--radius-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-bottom-line {
|
||||||
|
border-bottom: 1px solid var(--clr-theme-container-outline-light);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -84,6 +84,8 @@
|
|||||||
|
|
||||||
transition: background var(--transition-fast);
|
transition: background var(--transition-fast);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: color-mix(
|
background-color: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
@ -98,11 +100,15 @@
|
|||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
border-left-width: 1px;
|
border-left-width: 1px;
|
||||||
|
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
& > .label {
|
& > .label {
|
||||||
color: var(--clr-theme-scale-ntrl-0);
|
color: var(--clr-theme-scale-ntrl-0);
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
& > .icon {
|
& > .icon {
|
||||||
color: var(--clr-theme-scale-ntrl-30);
|
color: var(--clr-theme-scale-ntrl-30);
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
&.left {
|
&.left {
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
@ -128,9 +134,11 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--clr-theme-scale-ntrl-50);
|
color: var(--clr-theme-scale-ntrl-50);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--clr-theme-scale-ntrl-40);
|
color: var(--clr-theme-scale-ntrl-40);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
.divider {
|
.divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid var(--clr-theme-container-outline-light);
|
border-bottom: 1px solid var(--clr-theme-scale-ntrl-0);
|
||||||
|
opacity: 0.15;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="sync-btn"
|
class="sync-btn"
|
||||||
|
class:sync-btn-busy={$baseServiceBusy$}
|
||||||
on:click={async (e) => {
|
on:click={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -53,6 +54,12 @@
|
|||||||
background: var(--clr-theme-container-light);
|
background: var(--clr-theme-container-light);
|
||||||
border: 1px solid var(--clr-theme-container-outline-light);
|
border: 1px solid var(--clr-theme-container-outline-light);
|
||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.sync-btn-busy {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
transition:
|
transition:
|
||||||
background var(--transition-fast),
|
background var(--transition-fast),
|
||||||
border var(--transition-fast);
|
border var(--transition-fast);
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
padding: 0 var(--space-2);
|
padding: 0 var(--space-2);
|
||||||
}
|
}
|
||||||
.clickable {
|
.clickable {
|
||||||
cursor: default;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* colors */
|
/* colors */
|
||||||
|
@ -10,12 +10,19 @@
|
|||||||
export let autocomplete: string | undefined = undefined;
|
export let autocomplete: string | undefined = undefined;
|
||||||
export let autocorrect: string | undefined = undefined;
|
export let autocorrect: string | undefined = undefined;
|
||||||
export let spellcheck = false;
|
export let spellcheck = false;
|
||||||
|
export let label: string | undefined = undefined;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ input: string; change: string }>();
|
const dispatch = createEventDispatcher<{ input: string; change: string }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<textarea
|
<div class="textarea-wrapper">
|
||||||
class="text-input textarea"
|
{#if label}
|
||||||
|
<label class="textbox__label text-base-13 text-semibold" for={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
<textarea
|
||||||
|
class="text-input text-base-body-13 textarea"
|
||||||
bind:value
|
bind:value
|
||||||
{disabled}
|
{disabled}
|
||||||
{id}
|
{id}
|
||||||
@ -28,13 +35,24 @@
|
|||||||
{spellcheck}
|
{spellcheck}
|
||||||
on:input={(e) => dispatch('input', e.currentTarget.value)}
|
on:input={(e) => dispatch('input', e.currentTarget.value)}
|
||||||
on:change={(e) => dispatch('change', e.currentTarget.value)}
|
on:change={(e) => dispatch('change', e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
.textarea-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
.textarea {
|
.textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
resize: none;
|
resize: none;
|
||||||
padding-top: var(--space-12);
|
padding-top: var(--space-12);
|
||||||
padding-bottom: var(--space-12);
|
padding-bottom: var(--space-12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textbox__label {
|
||||||
|
color: var(--clr-theme-scale-ntrl-50);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -3,27 +3,29 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import type iconsJson from '$lib/icons/icons.json';
|
import type iconsJson from '$lib/icons/icons.json';
|
||||||
|
|
||||||
|
export let element: HTMLElement | undefined = undefined;
|
||||||
export let id: string | undefined = undefined; // Required to make label clickable
|
export let id: string | undefined = undefined; // Required to make label clickable
|
||||||
export let icon: keyof typeof iconsJson | undefined = undefined;
|
export let icon: keyof typeof iconsJson | undefined = undefined;
|
||||||
export let value: string | undefined = undefined;
|
export let value: string | undefined = undefined;
|
||||||
export let placeholder: string | undefined = undefined;
|
export let placeholder: string | undefined = undefined;
|
||||||
export let label: string | undefined = undefined;
|
export let label: string | undefined = undefined;
|
||||||
export let reversedDirection: boolean = false;
|
export let reversedDirection: boolean = false;
|
||||||
|
export let wide: boolean = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let required = false;
|
export let required = false;
|
||||||
export let noselect = false;
|
export let noselect = false;
|
||||||
export let selectall = false;
|
export let selectall = false;
|
||||||
export let element: HTMLElement | undefined = undefined;
|
|
||||||
export let spellcheck = false;
|
export let spellcheck = false;
|
||||||
|
|
||||||
export let type: 'text' | 'password' | 'select' = 'text';
|
export let type: 'text' | 'password' | 'select' = 'text';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ input: string; change: string }>();
|
const dispatch = createEventDispatcher<{ input: string; change: string }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="textbox" bind:this={element}>
|
<div class="textbox" bind:this={element} class:wide>
|
||||||
{#if label}
|
{#if label}
|
||||||
<label class="textbox__label font-base-13 text-semibold" for={id}>
|
<label class="textbox__label text-base-13 text-semibold" for={id}>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
@ -38,6 +40,7 @@
|
|||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
{id}
|
{id}
|
||||||
{readonly}
|
{readonly}
|
||||||
@ -47,7 +50,7 @@
|
|||||||
{disabled}
|
{disabled}
|
||||||
{...{ type }}
|
{...{ type }}
|
||||||
class="text-input textbox__input text-base-13"
|
class="text-input textbox__input text-base-13"
|
||||||
class:textbox__readonly={type !== 'select' && readonly}
|
class:textbox__readonly={type != 'select' && readonly}
|
||||||
class:select-none={noselect}
|
class:select-none={noselect}
|
||||||
class:select-all={selectall}
|
class:select-all={selectall}
|
||||||
bind:value
|
bind:value
|
||||||
@ -71,6 +74,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.textbox__input {
|
.textbox__input {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: var(--size-btn-l);
|
height: var(--size-btn-l);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -82,11 +87,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textbox__input[type='select'] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.textbox__label {
|
.textbox__label {
|
||||||
color: var(--clr-theme-scale-ntrl-50);
|
color: var(--clr-theme-scale-ntrl-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.textbox__icon {
|
.textbox__icon {
|
||||||
|
z-index: 2;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@ -118,4 +128,9 @@
|
|||||||
background-color: var(--clr-theme-container-pale);
|
background-color: var(--clr-theme-container-pale);
|
||||||
border-color: var(--clr-theme-container-outline-light);
|
border-color: var(--clr-theme-container-outline-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-card {
|
.theme-card {
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -78,14 +79,14 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: var(--radius-l);
|
border-radius: var(--radius-m);
|
||||||
border: 1px solid var(--clr-theme-container-outline-light);
|
border: 1px solid var(--clr-theme-container-outline-light);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& img {
|
& img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: auto;
|
||||||
object-fit: cover;
|
border-radius: var(--radius-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +115,10 @@
|
|||||||
|
|
||||||
.theme-card.selected .theme-card__preview {
|
.theme-card.selected .theme-card__preview {
|
||||||
border-color: var(--clr-core-pop-50);
|
border-color: var(--clr-core-pop-50);
|
||||||
border-width: 2px;
|
}
|
||||||
|
|
||||||
|
.theme-card.selected .theme-card__label {
|
||||||
|
background-color: color-mix(in srgb, var(--clr-theme-scale-pop-50), transparent 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-card.selected .theme-card__icon {
|
.theme-card.selected .theme-card__icon {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tooltip } from '$lib/utils/tooltip';
|
import { tooltip } from '$lib/utils/tooltip';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
export let name = '';
|
export let name = '';
|
||||||
|
|
||||||
@ -8,12 +9,18 @@
|
|||||||
export let checked = false;
|
export let checked = false;
|
||||||
export let value = '';
|
export let value = '';
|
||||||
export let help = '';
|
export let help = '';
|
||||||
|
|
||||||
|
let input: HTMLInputElement;
|
||||||
|
const dispatch = createEventDispatcher<{ change: boolean }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
bind:this={input}
|
||||||
|
bind:checked
|
||||||
on:click|stopPropagation
|
on:click|stopPropagation
|
||||||
on:change
|
on:change={() => {
|
||||||
use:tooltip={help}
|
dispatch('change', checked);
|
||||||
|
}}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle"
|
class="toggle"
|
||||||
class:small
|
class:small
|
||||||
@ -21,12 +28,13 @@
|
|||||||
{name}
|
{name}
|
||||||
id={name}
|
id={name}
|
||||||
{disabled}
|
{disabled}
|
||||||
bind:checked
|
use:tooltip={help}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.toggle {
|
.toggle {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
width: calc(var(--space-24) + var(--space-2));
|
width: calc(var(--space-24) + var(--space-2));
|
||||||
height: var(--space-16);
|
height: var(--space-16);
|
||||||
border-radius: var(--space-16);
|
border-radius: var(--space-16);
|
||||||
|
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} />
|
<Icon name={icon} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="label text-base-12">
|
<span class="label text-base-12">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</span>
|
||||||
<slot name="control" />
|
<slot name="control" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.menu-item {
|
.menu-item {
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -54,6 +55,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
|
user-select: none;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<section class="content-wrapper">
|
<section class="content-wrapper">
|
||||||
<ScrollableContainer>
|
<ScrollableContainer>
|
||||||
|
<div class="drag-region" data-tauri-drag-region>
|
||||||
<div class="content" data-tauri-drag-region>
|
<div class="content" data-tauri-drag-region>
|
||||||
{#if title}
|
{#if title}
|
||||||
<h1 class="title text-head-24">
|
<h1 class="title text-head-24">
|
||||||
@ -14,6 +15,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -23,7 +25,12 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: var(--clr-theme-container-light);
|
background-color: var(--clr-theme-container-pale);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-region {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
@ -119,7 +119,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: calc(var(--space-40) + var(--space-4)) var(--space-20) var(--space-20) var(--space-20);
|
padding: calc(var(--space-40) + var(--space-4)) var(--space-20) var(--space-20) var(--space-20);
|
||||||
border-right: 1px solid var(--clr-theme-container-outline-light);
|
border-right: 1px solid var(--clr-theme-container-outline-light);
|
||||||
background-color: var(--clr-theme-container-pale);
|
background-color: var(--clr-theme-container-light);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 16rem;
|
width: 16rem;
|
||||||
}
|
}
|
||||||
@ -172,7 +172,7 @@
|
|||||||
transition: none;
|
transition: none;
|
||||||
background-color: color-mix(
|
background-color: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--clr-theme-container-pale),
|
var(--clr-theme-container-light),
|
||||||
var(--darken-tint-light)
|
var(--darken-tint-light)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -183,7 +183,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item_selected {
|
.item_selected {
|
||||||
background-color: color-mix(in srgb, var(--clr-theme-container-pale), var(--darken-tint-light));
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--clr-theme-container-light),
|
||||||
|
var(--darken-tint-light)
|
||||||
|
);
|
||||||
color: var(--clr-theme-scale-ntrl-0);
|
color: var(--clr-theme-scale-ntrl-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +213,7 @@
|
|||||||
padding: var(--space-16);
|
padding: var(--space-16);
|
||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
border: 1px solid var(--clr-theme-container-outline-light);
|
border: 1px solid var(--clr-theme-container-outline-light);
|
||||||
background-color: var(--clr-theme-container-pale);
|
background-color: var(--clr-theme-container-light);
|
||||||
color: var(--clr-theme-scale-ntrl-30);
|
color: var(--clr-theme-scale-ntrl-30);
|
||||||
transition: background-color var(--transition-fast);
|
transition: background-color var(--transition-fast);
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
type PrStatus,
|
type PrStatus,
|
||||||
MergeMethod
|
MergeMethod
|
||||||
} from '$lib/github/types';
|
} from '$lib/github/types';
|
||||||
import { showToast, type ToastMessage } from '$lib/notifications/toasts';
|
import { showToast, type Toast } from '$lib/notifications/toasts';
|
||||||
import { sleep } from '$lib/utils/sleep';
|
import { sleep } from '$lib/utils/sleep';
|
||||||
import * as toasts from '$lib/utils/toasts';
|
import * as toasts from '$lib/utils/toasts';
|
||||||
import lscache from 'lscache';
|
import lscache from 'lscache';
|
||||||
@ -496,7 +496,7 @@ function loadPrs(
|
|||||||
* "status": 422
|
* "status": 422
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
function mapErrorToToast(err: any): ToastMessage | undefined {
|
function mapErrorToToast(err: any): Toast | undefined {
|
||||||
// We expect an object to be thrown by octokit.
|
// We expect an object to be thrown by octokit.
|
||||||
if (typeof err != 'object') return;
|
if (typeof err != 'object') return;
|
||||||
|
|
||||||
|
@ -84,5 +84,7 @@
|
|||||||
"git": "M9.23744 1.17678C8.55402 0.49336 7.44598 0.493358 6.76257 1.17678L1.17678 6.76256C0.493362 7.44598 0.49336 8.55402 1.17678 9.23744L6.76257 14.8232C7.44598 15.5066 8.55402 15.5066 9.23744 14.8232L14.8232 9.23744C15.5066 8.55402 15.5066 7.44598 14.8232 6.76256L9.23744 1.17678ZM7.82323 2.23744C7.92086 2.13981 8.07915 2.13981 8.17678 2.23744L13.7626 7.82322C13.8602 7.92085 13.8602 8.07914 13.7626 8.17678L8.17678 13.7626C8.07915 13.8602 7.92086 13.8602 7.82323 13.7626L2.23744 8.17678C2.13981 8.07915 2.13981 7.92086 2.23744 7.82322L5.5 4.56066L7.43934 6.5L4.96967 8.96967L6.03033 10.0303L8.5 7.56066L10.4697 9.53033L11.5303 8.46967L9.03033 5.96967L6.56066 3.5L7.82323 2.23744Z",
|
"git": "M9.23744 1.17678C8.55402 0.49336 7.44598 0.493358 6.76257 1.17678L1.17678 6.76256C0.493362 7.44598 0.49336 8.55402 1.17678 9.23744L6.76257 14.8232C7.44598 15.5066 8.55402 15.5066 9.23744 14.8232L14.8232 9.23744C15.5066 8.55402 15.5066 7.44598 14.8232 6.76256L9.23744 1.17678ZM7.82323 2.23744C7.92086 2.13981 8.07915 2.13981 8.17678 2.23744L13.7626 7.82322C13.8602 7.92085 13.8602 8.07914 13.7626 8.17678L8.17678 13.7626C8.07915 13.8602 7.92086 13.8602 7.82323 13.7626L2.23744 8.17678C2.13981 8.07915 2.13981 7.92086 2.23744 7.82322L5.5 4.56066L7.43934 6.5L4.96967 8.96967L6.03033 10.0303L8.5 7.56066L10.4697 9.53033L11.5303 8.46967L9.03033 5.96967L6.56066 3.5L7.82323 2.23744Z",
|
||||||
"chevron-left": "M6.23744 8.17678C6.13981 8.07914 6.13981 7.92085 6.23744 7.82322L10.5303 3.53033L9.46967 2.46967L5.17678 6.76256C4.49336 7.44598 4.49336 8.55402 5.17678 9.23744L9.46967 13.5303L10.5303 12.4697L6.23744 8.17678Z",
|
"chevron-left": "M6.23744 8.17678C6.13981 8.07914 6.13981 7.92085 6.23744 7.82322L10.5303 3.53033L9.46967 2.46967L5.17678 6.76256C4.49336 7.44598 4.49336 8.55402 5.17678 9.23744L9.46967 13.5303L10.5303 12.4697L6.23744 8.17678Z",
|
||||||
"chevron-right": "M9.76256 8.17678C9.86019 8.07914 9.86019 7.92085 9.76256 7.82322L5.46967 3.53033L6.53033 2.46967L10.8232 6.76256C11.5066 7.44598 11.5066 8.55402 10.8232 9.23744L6.53033 13.5303L5.46967 12.4697L9.76256 8.17678Z",
|
"chevron-right": "M9.76256 8.17678C9.86019 8.07914 9.86019 7.92085 9.76256 7.82322L5.46967 3.53033L6.53033 2.46967L10.8232 6.76256C11.5066 7.44598 11.5066 8.55402 10.8232 9.23744L6.53033 13.5303L5.46967 12.4697L9.76256 8.17678Z",
|
||||||
"github": "M8.00579 1C4.13177 1 1 4.20832 1 8.17745C1 11.3502 3.00663 14.0358 5.79036 14.9864C6.1384 15.0578 6.26589 14.8319 6.26589 14.6419C6.26589 14.4755 6.25442 13.9052 6.25442 13.3109C4.30557 13.7388 3.89974 12.4553 3.89974 12.4553C3.58655 11.6235 3.1225 11.4097 3.1225 11.4097C2.48465 10.97 3.16896 10.97 3.16896 10.97C3.87651 11.0175 4.24778 11.7067 4.24778 11.7067C4.87402 12.7999 5.88315 12.491 6.28912 12.3009C6.34705 11.8374 6.53276 11.5166 6.72994 11.3384C5.1756 11.172 3.54023 10.5541 3.54023 7.79712C3.54023 7.01283 3.81844 6.37117 4.25926 5.87213C4.1897 5.69392 3.94606 4.95703 4.32895 3.97076C4.32895 3.97076 4.92048 3.78059 6.25427 4.70751C6.82531 4.55039 7.41422 4.47047 8.00579 4.4698C8.59733 4.4698 9.20034 4.55307 9.75717 4.70751C11.0911 3.78059 11.6826 3.97076 11.6826 3.97076C12.0655 4.95703 11.8217 5.69392 11.7522 5.87213C12.2046 6.37117 12.4714 7.01283 12.4714 7.79712C12.4714 10.5541 10.836 11.16 9.27003 11.3384C9.52529 11.5641 9.74555 11.9919 9.74555 12.6692C9.74555 13.6317 9.73408 14.4042 9.73408 14.6418C9.73408 14.8319 9.86171 15.0578 10.2096 14.9865C12.9933 14.0357 15 11.3502 15 8.17745C15.0114 4.20832 11.8682 1 8.00579 1Z"
|
"github": "M8.00579 1C4.13177 1 1 4.20832 1 8.17745C1 11.3502 3.00663 14.0358 5.79036 14.9864C6.1384 15.0578 6.26589 14.8319 6.26589 14.6419C6.26589 14.4755 6.25442 13.9052 6.25442 13.3109C4.30557 13.7388 3.89974 12.4553 3.89974 12.4553C3.58655 11.6235 3.1225 11.4097 3.1225 11.4097C2.48465 10.97 3.16896 10.97 3.16896 10.97C3.87651 11.0175 4.24778 11.7067 4.24778 11.7067C4.87402 12.7999 5.88315 12.491 6.28912 12.3009C6.34705 11.8374 6.53276 11.5166 6.72994 11.3384C5.1756 11.172 3.54023 10.5541 3.54023 7.79712C3.54023 7.01283 3.81844 6.37117 4.25926 5.87213C4.1897 5.69392 3.94606 4.95703 4.32895 3.97076C4.32895 3.97076 4.92048 3.78059 6.25427 4.70751C6.82531 4.55039 7.41422 4.47047 8.00579 4.4698C8.59733 4.4698 9.20034 4.55307 9.75717 4.70751C11.0911 3.78059 11.6826 3.97076 11.6826 3.97076C12.0655 4.95703 11.8217 5.69392 11.7522 5.87213C12.2046 6.37117 12.4714 7.01283 12.4714 7.79712C12.4714 10.5541 10.836 11.16 9.27003 11.3384C9.52529 11.5641 9.74555 11.9919 9.74555 12.6692C9.74555 13.6317 9.73408 14.4042 9.73408 14.6418C9.73408 14.8319 9.86171 15.0578 10.2096 14.9865C12.9933 14.0357 15 11.3502 15 8.17745C15.0114 4.20832 11.8682 1 8.00579 1Z",
|
||||||
|
"eye-shown": "M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z M7.99998 3.25C4.94324 3.25 2.37688 5.33566 1.31608 7.69213L1.17749 8L1.31608 8.30786C2.37687 10.6643 4.94324 12.75 7.99998 12.75C11.0567 12.75 13.6231 10.6643 14.6839 8.30787L14.8225 8.00001L14.6839 7.69214C13.6231 5.33566 11.0567 3.25 7.99998 3.25ZM7.99998 11.25C5.75461 11.25 3.76954 9.7742 2.83424 8C3.76954 6.2258 5.75462 4.75 7.99998 4.75C10.2453 4.75 12.2304 6.2258 13.1657 8C12.2304 9.7742 10.2453 11.25 7.99998 11.25Z",
|
||||||
|
"eye-hidden": "M10.3292 1.66459L4.32919 13.6646L5.67083 14.3354L6.54358 12.5899C7.01299 12.6938 7.50015 12.75 8.00001 12.75C11.0567 12.75 13.6231 10.6643 14.6839 8.30787L14.8225 8L14.6839 7.69213C13.9768 6.12144 12.6133 4.68765 10.8911 3.89486L11.6708 2.33541L10.3292 1.66459ZM10.2198 5.23744L7.24143 11.1942C7.49004 11.2309 7.74323 11.25 8.00001 11.25C10.2453 11.25 12.2304 9.77424 13.1657 8.00008C12.5601 6.85225 11.5063 5.817 10.2198 5.23744Z M7.1258 4.82412C5.24551 5.14401 3.6433 6.46644 2.83436 7.99994C3.17611 8.64745 3.66354 9.26476 4.26171 9.78496L3.27737 10.9168C2.42955 10.1795 1.75091 9.27374 1.31611 8.30786L1.17752 7.99999L1.31611 7.69213C2.24746 5.62322 4.32664 3.77878 6.87422 3.34537L7.1258 4.82412Z"
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
bottom: var(--space-20);
|
bottom: var(--space-20);
|
||||||
right: var(--space-20);
|
right: var(--space-20);
|
||||||
gap: var(--space-8);
|
gap: var(--space-8);
|
||||||
max-width: 22rem;
|
max-width: 30rem;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -2,22 +2,26 @@ import { writable, type Writable } from 'svelte/store';
|
|||||||
|
|
||||||
export type ToastStyle = 'neutral' | 'error' | 'pop' | 'warn';
|
export type ToastStyle = 'neutral' | 'error' | 'pop' | 'warn';
|
||||||
|
|
||||||
export interface ToastMessage {
|
export interface Toast {
|
||||||
id?: number;
|
id?: string;
|
||||||
message: string;
|
message: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
style?: ToastStyle;
|
style?: ToastStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toastStore: Writable<ToastMessage[]> = writable([]);
|
export const toastStore: Writable<Toast[]> = writable([]);
|
||||||
|
|
||||||
let idCounter = 0;
|
let idCounter = 0;
|
||||||
|
|
||||||
export function showToast(message: ToastMessage) {
|
export function showToast(toast: Toast) {
|
||||||
message.message = message.message.replace(/^ */gm, '');
|
toast.message = toast.message.replace(/^ */gm, '');
|
||||||
toastStore.update((items) => [...items, { id: idCounter++, ...message }]);
|
toastStore.update((items) => [
|
||||||
|
...items.filter((t) => toast.id == undefined || t.id != toast.id),
|
||||||
|
{ id: (idCounter++).toString(), ...toast }
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dismissToast(messageId: number | undefined) {
|
export function dismissToast(messageId: string | undefined) {
|
||||||
|
if (!messageId) return;
|
||||||
toastStore.update((items) => items.filter((m) => m.id != messageId));
|
toastStore.update((items) => items.filter((m) => m.id != messageId));
|
||||||
}
|
}
|
||||||
|
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 { invoke } from '$lib/backend/ipc';
|
||||||
|
import { showToast } from '$lib/notifications/toasts';
|
||||||
import * as toasts from '$lib/utils/toasts';
|
import * as toasts from '$lib/utils/toasts';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||||
@ -61,7 +62,7 @@ export class BranchController {
|
|||||||
});
|
});
|
||||||
posthog.capture('Commit Successful');
|
posthog.capture('Commit Successful');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toasts.error('Failed to commit branch');
|
toasts.error('Failed to commit changes');
|
||||||
posthog.capture('Commit Failed', err);
|
posthog.capture('Commit Failed', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,10 +194,33 @@ export class BranchController {
|
|||||||
await this.vbranchService.reload();
|
await this.vbranchService.reload();
|
||||||
return await this.vbranchService.getById(branchId);
|
return await this.vbranchService.getById(branchId);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
if (err.code === 'errors.git.authentication') {
|
if (err.code === 'errors.git.authentication') {
|
||||||
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?');
|
showToast({
|
||||||
|
title: 'Git push failed',
|
||||||
|
message: `
|
||||||
|
Your branch cannot be pushed due to an authentication failure.
|
||||||
|
|
||||||
|
Please check our [documentation](https://docs.gitbutler.com/troubleshooting/fetch-push)
|
||||||
|
on fetching and pushing for ways to resolve the problem.
|
||||||
|
|
||||||
|
\`\`\`${err.message}\`\`\`
|
||||||
|
`,
|
||||||
|
style: 'error'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
toasts.error(`Failed to push branch: ${err.message}`);
|
showToast({
|
||||||
|
title: 'Git push failed',
|
||||||
|
message: `
|
||||||
|
Your branch cannot be pushed due to an unforeseen problem.
|
||||||
|
|
||||||
|
Please check our [documentation](https://docs.gitbutler.com/troubleshooting/fetch-push)
|
||||||
|
on fetching and pushing for ways to resolve the problem.
|
||||||
|
|
||||||
|
\`\`\`${err.message}\`\`\`
|
||||||
|
`,
|
||||||
|
style: 'error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { BaseBranch, Branch } from './types';
|
import { BaseBranch, Branch } from './types';
|
||||||
import { invoke, listen } from '$lib/backend/ipc';
|
import { Code, invoke, listen } from '$lib/backend/ipc';
|
||||||
import * as toasts from '$lib/utils/toasts';
|
import * as toasts from '$lib/utils/toasts';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import posthog from 'posthog-js';
|
|
||||||
import {
|
import {
|
||||||
switchMap,
|
switchMap,
|
||||||
Observable,
|
Observable,
|
||||||
@ -101,26 +100,6 @@ export class VirtualBranchService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushBranch(branchId: string, withForce: boolean): Promise<Branch | undefined> {
|
|
||||||
try {
|
|
||||||
await invoke<void>('push_virtual_branch', {
|
|
||||||
projectId: this.projectId,
|
|
||||||
branchId,
|
|
||||||
withForce
|
|
||||||
});
|
|
||||||
posthog.capture('Push Successful');
|
|
||||||
await this.reload();
|
|
||||||
return await this.getById(branchId);
|
|
||||||
} catch (err: any) {
|
|
||||||
posthog.capture('Push Failed', { error: err });
|
|
||||||
if (err.code === 'errors.git.authentication') {
|
|
||||||
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?');
|
|
||||||
} else {
|
|
||||||
toasts.error(`Failed to push branch: ${err.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToVirtualBranches(projectId: string, callback: (branches: Branch[]) => void) {
|
function subscribeToVirtualBranches(projectId: string, callback: (branches: Branch[]) => void) {
|
||||||
@ -147,6 +126,9 @@ export class BaseBranchService {
|
|||||||
return await getBaseBranch({ projectId });
|
return await getBaseBranch({ projectId });
|
||||||
}),
|
}),
|
||||||
tap(() => this.busy$.next(false)),
|
tap(() => this.busy$.next(false)),
|
||||||
|
// Start with undefined to prevent delay in updating $baseBranch$ value in
|
||||||
|
// layout.svelte when navigating between projects.
|
||||||
|
startWith(undefined),
|
||||||
shareReplay(1),
|
shareReplay(1),
|
||||||
catchError((e) => {
|
catchError((e) => {
|
||||||
this.error$.next(e);
|
this.error$.next(e);
|
||||||
@ -162,11 +144,15 @@ export class BaseBranchService {
|
|||||||
// trigger a base branch reload. It feels a bit awkward and should be improved.
|
// trigger a base branch reload. It feels a bit awkward and should be improved.
|
||||||
await invoke<void>('fetch_from_target', { projectId: this.projectId });
|
await invoke<void>('fetch_from_target', { projectId: this.projectId });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.code === 'errors.git.authentication') {
|
if (err.message?.includes('does not have a default target')) {
|
||||||
|
// Swallow this error since user should be taken to project setup page
|
||||||
|
return;
|
||||||
|
} else if (err.code === Code.ProjectsGitAuth) {
|
||||||
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?');
|
toasts.error('Failed to authenticate. Did you setup GitButler ssh keys?');
|
||||||
} else {
|
} else {
|
||||||
toasts.error(`Failed to fetch branch: ${err.message}`);
|
toasts.error(`Failed to fetch branch: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,6 +290,13 @@ export class BaseBranch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
commitUrl(commitId: string): string | undefined {
|
commitUrl(commitId: string): string | undefined {
|
||||||
|
// if repoBaseUrl is bitbucket, then the commit url is different
|
||||||
|
if (this.repoBaseUrl.includes('bitbucket.org')) {
|
||||||
|
return `${this.repoBaseUrl}/commits/${commitId}`;
|
||||||
|
}
|
||||||
|
if (this.repoBaseUrl.includes('gitlab.com')) {
|
||||||
|
return `${this.repoBaseUrl}/-/commit/${commitId}`;
|
||||||
|
}
|
||||||
return `${this.repoBaseUrl}/commit/${commitId}`;
|
return `${this.repoBaseUrl}/commit/${commitId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import '../styles/main.postcss';
|
import '../styles/main.postcss';
|
||||||
|
|
||||||
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
|
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
|
||||||
|
import UpdateButton from '$lib/components/UpdateButton.svelte';
|
||||||
import ToastController from '$lib/notifications/ToastController.svelte';
|
import ToastController from '$lib/notifications/ToastController.svelte';
|
||||||
import { SETTINGS_CONTEXT, loadUserSettings } from '$lib/settings/userSettings';
|
import { SETTINGS_CONTEXT, loadUserSettings } from '$lib/settings/userSettings';
|
||||||
import * as events from '$lib/utils/events';
|
import * as events from '$lib/utils/events';
|
||||||
@ -14,7 +15,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
export let data: LayoutData;
|
export let data: LayoutData;
|
||||||
const { cloud, user$ } = data;
|
const { cloud, user$, updaterService } = data;
|
||||||
|
|
||||||
const userSettings = loadUserSettings();
|
const userSettings = loadUserSettings();
|
||||||
initTheme(userSettings);
|
initTheme(userSettings);
|
||||||
@ -51,3 +52,4 @@
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<ShareIssueModal bind:this={shareIssueModal} user={$user$} {cloud} />
|
<ShareIssueModal bind:this={shareIssueModal} user={$user$} {cloud} />
|
||||||
<ToastController />
|
<ToastController />
|
||||||
|
<UpdateButton {updaterService} />
|
||||||
|
@ -2,6 +2,7 @@ import { initPostHog } from '$lib/analytics/posthog';
|
|||||||
import { initSentry } from '$lib/analytics/sentry';
|
import { initSentry } from '$lib/analytics/sentry';
|
||||||
import { getCloudApiClient } from '$lib/backend/cloud';
|
import { getCloudApiClient } from '$lib/backend/cloud';
|
||||||
import { ProjectService } from '$lib/backend/projects';
|
import { ProjectService } from '$lib/backend/projects';
|
||||||
|
import { UpdaterService } from '$lib/backend/updater';
|
||||||
import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings';
|
import { appMetricsEnabled, appErrorReportingEnabled } from '$lib/config/appSettings';
|
||||||
import { UserService } from '$lib/stores/user';
|
import { UserService } from '$lib/stores/user';
|
||||||
import lscache from 'lscache';
|
import lscache from 'lscache';
|
||||||
@ -32,6 +33,7 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
|
|||||||
if (enabled) initPostHog();
|
if (enabled) initPostHog();
|
||||||
});
|
});
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
const updaterService = new UpdaterService();
|
||||||
|
|
||||||
// TODO: Find a workaround to avoid this dynamic import
|
// TODO: Find a workaround to avoid this dynamic import
|
||||||
// https://github.com/sveltejs/kit/issues/905
|
// https://github.com/sveltejs/kit/issues/905
|
||||||
@ -41,6 +43,7 @@ export const load: LayoutLoad = async ({ fetch: realFetch }: { fetch: typeof fet
|
|||||||
return {
|
return {
|
||||||
projectService: new ProjectService(defaultPath),
|
projectService: new ProjectService(defaultPath),
|
||||||
cloud: getCloudApiClient({ fetch: realFetch }),
|
cloud: getCloudApiClient({ fetch: realFetch }),
|
||||||
|
updaterService,
|
||||||
userService,
|
userService,
|
||||||
user$: userService.user$
|
user$: userService.user$
|
||||||
};
|
};
|
||||||
|
@ -9,11 +9,8 @@
|
|||||||
import * as hotkeys from '$lib/utils/hotkeys';
|
import * as hotkeys from '$lib/utils/hotkeys';
|
||||||
import { unsubscribe } from '$lib/utils/random';
|
import { unsubscribe } from '$lib/utils/random';
|
||||||
import { getRemoteBranches } from '$lib/vbranches/branchStoresCache';
|
import { getRemoteBranches } from '$lib/vbranches/branchStoresCache';
|
||||||
import { interval, Subscription } from 'rxjs';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { startWith, tap } from 'rxjs/operators';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import type { LayoutData } from './$types';
|
import type { LayoutData } from './$types';
|
||||||
import { page } from '$app/stores';
|
|
||||||
|
|
||||||
export let data: LayoutData;
|
export let data: LayoutData;
|
||||||
|
|
||||||
@ -35,28 +32,31 @@
|
|||||||
$: user$ = data.user$;
|
$: user$ = data.user$;
|
||||||
|
|
||||||
let trayViewport: HTMLElement;
|
let trayViewport: HTMLElement;
|
||||||
|
let intervalId: any;
|
||||||
handleMenuActions(data.projectId);
|
handleMenuActions(data.projectId);
|
||||||
|
|
||||||
let lastProjectId: string | undefined = undefined;
|
// Once on load and every time the project id changes
|
||||||
onMount(() => {
|
$: if (projectId) setupFetchInterval();
|
||||||
let fetchSub: Subscription;
|
|
||||||
// Project is auto-fetched on page load and then every 15 minutes
|
function setupFetchInterval() {
|
||||||
page.subscribe((page) => {
|
baseBranchService.fetchFromTarget();
|
||||||
if (page.params.projectId !== lastProjectId) {
|
clearFetchInterval();
|
||||||
lastProjectId = page.params.projectId;
|
intervalId = setInterval(() => baseBranchService.fetchFromTarget(), 15 * 60 * 1000);
|
||||||
fetchSub?.unsubscribe();
|
|
||||||
fetchSub = interval(1000 * 60 * 15)
|
|
||||||
.pipe(
|
|
||||||
startWith(0),
|
|
||||||
tap(() => baseBranchService.fetchFromTarget())
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return unsubscribe(
|
function clearFetchInterval() {
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
unsubscribe(
|
||||||
menuSubscribe(data.projectId),
|
menuSubscribe(data.projectId),
|
||||||
hotkeys.on('Meta+Shift+S', () => syncToCloud($project$?.id))
|
hotkeys.on('Meta+Shift+S', () => syncToCloud(projectId))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearFetchInterval();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -65,8 +65,7 @@
|
|||||||
{:else if $baseError$}
|
{:else if $baseError$}
|
||||||
<ProblemLoadingRepo {projectService} {userService} project={$project$} error={$baseError$} />
|
<ProblemLoadingRepo {projectService} {userService} project={$project$} error={$baseError$} />
|
||||||
{:else if $baseBranch$ === null}
|
{:else if $baseBranch$ === null}
|
||||||
{@const remoteBranches = getRemoteBranches(projectId)}
|
{#await getRemoteBranches(projectId)}
|
||||||
{#await remoteBranches}
|
|
||||||
<p>loading...</p>
|
<p>loading...</p>
|
||||||
{:then remoteBranches}
|
{:then remoteBranches}
|
||||||
{#if remoteBranches.length == 0}
|
{#if remoteBranches.length == 0}
|
||||||
|
@ -34,7 +34,12 @@ export const load: LayoutLoad = async ({ params, parent }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const githubService = new GitHubService(userService, baseBranchService);
|
const githubService = new GitHubService(userService, baseBranchService);
|
||||||
const branchService = new BranchService(vbranchService, remoteBranchService, githubService);
|
const branchService = new BranchService(
|
||||||
|
vbranchService,
|
||||||
|
remoteBranchService,
|
||||||
|
githubService,
|
||||||
|
branchController
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CloudForm from '$lib/components/CloudForm.svelte';
|
import CloudForm from '$lib/components/CloudForm.svelte';
|
||||||
import DetailsForm from '$lib/components/DetailsForm.svelte';
|
import DetailsForm from '$lib/components/DetailsForm.svelte';
|
||||||
|
import FullscreenLoading from '$lib/components/FullscreenLoading.svelte';
|
||||||
import KeysForm from '$lib/components/KeysForm.svelte';
|
import KeysForm from '$lib/components/KeysForm.svelte';
|
||||||
import PreferencesForm from '$lib/components/PreferencesForm.svelte';
|
import PreferencesForm from '$lib/components/PreferencesForm.svelte';
|
||||||
import RemoveProjectButton from '$lib/components/RemoveProjectButton.svelte';
|
import RemoveProjectButton from '$lib/components/RemoveProjectButton.svelte';
|
||||||
import ScrollableContainer from '$lib/components/ScrollableContainer.svelte';
|
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||||
import Spacer from '$lib/components/Spacer.svelte';
|
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
|
||||||
import * as toasts from '$lib/utils/toasts';
|
import * as toasts from '$lib/utils/toasts';
|
||||||
import type { UserError } from '$lib/backend/ipc';
|
import type { UserError } from '$lib/backend/ipc';
|
||||||
import type { Key, Project } from '$lib/backend/projects';
|
import type { Key, Project } from '$lib/backend/projects';
|
||||||
@ -66,50 +67,21 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollableContainer wide>
|
{#if !$project$}
|
||||||
<div class="settings" data-tauri-drag-region>
|
<FullscreenLoading />
|
||||||
<div class="card">
|
{:else}
|
||||||
{#if !$project$}
|
<ContentWrapper title="Project settings">
|
||||||
loading...
|
|
||||||
{:else}
|
|
||||||
<div class="card__header">
|
|
||||||
<span class="card_title text-base-16 text-semibold">Project settings</span>
|
|
||||||
</div>
|
|
||||||
<div class="card__content">
|
|
||||||
<CloudForm project={$project$} user={$user$} {userService} on:updated={onCloudUpdated} />
|
<CloudForm project={$project$} user={$user$} {userService} on:updated={onCloudUpdated} />
|
||||||
<Spacer margin={2} />
|
|
||||||
<DetailsForm project={$project$} on:updated={onDetailsUpdated} />
|
<DetailsForm project={$project$} on:updated={onDetailsUpdated} />
|
||||||
<Spacer margin={2} />
|
|
||||||
<KeysForm project={$project$} on:updated={onKeysUpdated} />
|
<KeysForm project={$project$} on:updated={onKeysUpdated} />
|
||||||
<Spacer margin={2} />
|
|
||||||
<PreferencesForm project={$project$} on:updated={onPreferencesUpdated} />
|
<PreferencesForm project={$project$} on:updated={onPreferencesUpdated} />
|
||||||
<Spacer margin={2} />
|
<SectionCard>
|
||||||
|
<svelte:fragment slot="title">Remove project</svelte:fragment>
|
||||||
<div class="flex gap-x-4">
|
<svelte:fragment slot="body">
|
||||||
<a
|
You can remove projects from GitButler, your code remains safe as this only clears
|
||||||
href="https://discord.gg/wDKZCPEjXC"
|
configuration.
|
||||||
target="_blank"
|
</svelte:fragment>
|
||||||
rel="noreferrer"
|
<div>
|
||||||
class="flex-1 rounded border border-light-200 bg-white p-4 dark:border-dark-400 dark:bg-dark-700"
|
|
||||||
>
|
|
||||||
<p class="mb-2 font-medium">Join our Discord</p>
|
|
||||||
<p class="text-light-700 dark:text-dark-200">
|
|
||||||
Join our community and share feedback, requests, or ask a question.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="mailto:hello@gitbutler.com?subject=Feedback or question!"
|
|
||||||
target="_blank"
|
|
||||||
class="flex-1 rounded border border-light-200 bg-white p-4 dark:border-dark-400 dark:bg-dark-700"
|
|
||||||
>
|
|
||||||
<p class="mb-2 font-medium">Contact us</p>
|
|
||||||
<p class="text-light-700 dark:text-dark-200">
|
|
||||||
If you have an issue or any questions, contact us.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card__footer">
|
|
||||||
<RemoveProjectButton
|
<RemoveProjectButton
|
||||||
bind:this={deleteConfirmationModal}
|
bind:this={deleteConfirmationModal}
|
||||||
projectTitle={$project$?.title}
|
projectTitle={$project$?.title}
|
||||||
@ -117,23 +89,6 @@
|
|||||||
{onDeleteClicked}
|
{onDeleteClicked}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</SectionCard>
|
||||||
</div>
|
</ContentWrapper>
|
||||||
</div>
|
{/if}
|
||||||
</ScrollableContainer>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.settings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: var(--space-16) var(--space-16);
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
max-width: 50rem;
|
|
||||||
}
|
|
||||||
.card__content {
|
|
||||||
gap: var(--space-24);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -71,14 +71,6 @@ button {
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
div,
|
|
||||||
button,
|
|
||||||
[role='button'],
|
|
||||||
select,
|
|
||||||
a {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* SCROLL BAR STYLING */
|
/* SCROLL BAR STYLING */
|
||||||
/* We don't use REM here becasue we don't want
|
/* We don't use REM here becasue we don't want
|
||||||
the scrollbar to scale with the font size */
|
the scrollbar to scale with the font size */
|
||||||
|
Loading…
Reference in New Issue
Block a user