mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-30 20:09:50 +03:00
many trunk changes
This commit is contained in:
commit
c1364a1fe5
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@ -71,6 +71,10 @@ jobs:
|
||||
gpg_private_key: ${{ secrets.APPIMAGE_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.APPIMAGE_KEY_PASSPHRASE }}
|
||||
|
||||
- name: install linux dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Build binary
|
||||
run: |
|
||||
./scripts/release.sh \
|
||||
|
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -1876,6 +1876,8 @@ name = "gitbutler-git"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"git2",
|
||||
"nix 0.27.1",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@ -2881,7 +2883,7 @@ dependencies = [
|
||||
"combine",
|
||||
"libc",
|
||||
"mach2",
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"sysctl",
|
||||
"thiserror",
|
||||
"widestring",
|
||||
@ -2953,6 +2955,18 @@ dependencies = [
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
@ -6531,7 +6545,7 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@ -6565,7 +6579,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"once_cell",
|
||||
"ordered-stream",
|
||||
"rand 0.8.5",
|
||||
|
@ -3,9 +3,22 @@ name = "gitbutler-git"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "gitbutler-git-askpass"
|
||||
path = "src/backend/cli/bin/askpass.rs"
|
||||
required-features = ["cli"]
|
||||
|
||||
[[bin]]
|
||||
name = "gitbutler-git-setsid"
|
||||
path = "src/backend/cli/bin/setsid.rs"
|
||||
required-features = ["cli"]
|
||||
|
||||
[features]
|
||||
default = ["git2", "cli", "serde", "tokio"]
|
||||
cli = ["std"]
|
||||
cli = ["std", "dep:nix", "dep:rand"]
|
||||
git2 = ["dep:git2", "std"]
|
||||
serde = ["dep:serde"]
|
||||
std = ["dep:thiserror"]
|
||||
@ -16,6 +29,10 @@ git2 = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, optional = true, features = ["process"]}
|
||||
rand = { version = "0.8.5", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "process"]}
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
nix = { version = "0.27.1", optional = true, features = ["process", "socket"] }
|
||||
|
@ -22,7 +22,7 @@ mod tests {
|
||||
.join(test_name);
|
||||
let _ = std::fs::remove_dir_all(&repo_path);
|
||||
std::fs::create_dir_all(&repo_path).unwrap();
|
||||
Repository::open_or_init(executor::tokio::TokioExecutor, repo_path)
|
||||
Repository::open_or_init(executor::tokio::TokioExecutor, repo_path.to_str().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
36
gitbutler-git/src/backend/cli/bin/askpass-unix.rs
Normal file
36
gitbutler-git/src/backend/cli/bin/askpass-unix.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
pub fn main(sock_path: &str, secret: &str, prompt: &str) {
|
||||
let mut stream = UnixStream::connect(sock_path).expect("connect():");
|
||||
|
||||
// Set a timer for 10s.
|
||||
stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(10)))
|
||||
.expect("set_read_timeout():");
|
||||
|
||||
// Write the secret.
|
||||
stream
|
||||
.write_all(secret.as_bytes())
|
||||
.expect("write_all(secret):");
|
||||
|
||||
// Write the prompt that Git gave us.
|
||||
stream
|
||||
.write_all(prompt.as_bytes())
|
||||
.expect("write_all(prompt):");
|
||||
|
||||
// Wait for the response.
|
||||
let mut buf = [0; 2048];
|
||||
let n = stream.read(&mut buf).expect("read():");
|
||||
|
||||
// TODO(qix-): Figure out a way to do a single timeout
|
||||
// TODO(qix-): but allow any response size.
|
||||
if n == buf.len() {
|
||||
panic!("response too long");
|
||||
}
|
||||
|
||||
// Write the response back to Git.
|
||||
std::io::stdout()
|
||||
.write_all(&buf[..n])
|
||||
.expect("write_all(stdout):");
|
||||
}
|
15
gitbutler-git/src/backend/cli/bin/askpass.rs
Normal file
15
gitbutler-git/src/backend/cli/bin/askpass.rs
Normal file
@ -0,0 +1,15 @@
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[path = "askpass-unix.rs"]
|
||||
mod unix;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
compile_error!("Windows support is not yet implemented.");
|
||||
|
||||
pub fn main() {
|
||||
let pipe_name = std::env::var("GITBUTLER_ASKPASS_PIPE").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_PIPE env var)");
|
||||
let pipe_secret = std::env::var("GITBUTLER_ASKPASS_SECRET").expect("do not run this binary yourself; it's only meant to be run by GitButler (missing GITBUTLER_ASKPASS_SECRET env var)");
|
||||
let prompt = std::env::args().nth(1).expect("do not run this binary yourself; it's only meant to be run by GitButler (missing prompt arg)");
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
unix::main(&pipe_name, &pipe_secret, &prompt);
|
||||
}
|
54
gitbutler-git/src/backend/cli/bin/setsid.rs
Normal file
54
gitbutler-git/src/backend/cli/bin/setsid.rs
Normal file
@ -0,0 +1,54 @@
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use nix::{
|
||||
libc::{c_int, wait, EXIT_FAILURE, WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG},
|
||||
unistd::{fork, setsid, ForkResult},
|
||||
};
|
||||
use std::{os::unix::process::CommandExt, process};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn main() {
|
||||
panic!("This binary is only meant to be run on Unix-like systems. It exists on Windows only because Cargo cannot switch off bins based on target platform.");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn main() {
|
||||
let has_pipe_var = std::env::var("GITBUTLER_ASKPASS_PIPE")
|
||||
.map(|v| v != "")
|
||||
.unwrap_or(false);
|
||||
if !has_pipe_var {
|
||||
panic!("This binary is only meant to be run by GitButler; please do not use it yourself as it's entirely unstable.");
|
||||
}
|
||||
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
match unsafe { fork() }.unwrap() {
|
||||
ForkResult::Parent { child, .. } => {
|
||||
let mut status: c_int = 0;
|
||||
|
||||
let waited_pid = unsafe { wait(&mut status as *mut _) };
|
||||
if waited_pid != child.as_raw() {
|
||||
panic!(
|
||||
"wait(): unexpected child process; got {}, expected {}",
|
||||
waited_pid, child
|
||||
);
|
||||
}
|
||||
|
||||
if WIFEXITED(status) {
|
||||
let exit_status = WEXITSTATUS(status);
|
||||
process::exit(exit_status);
|
||||
} else if WIFSIGNALED(status) {
|
||||
let signal = WTERMSIG(status);
|
||||
process::exit(128 + signal);
|
||||
} else {
|
||||
process::exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
ForkResult::Child => {
|
||||
setsid().expect("setsid():");
|
||||
|
||||
let err = process::Command::new(&args[0]).args(&args[1..]).exec();
|
||||
|
||||
panic!("exec(): {}", err);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
#[cfg(any(test, feature = "tokio"))]
|
||||
pub mod tokio;
|
||||
|
||||
@ -21,5 +23,32 @@ pub trait GitExecutor {
|
||||
///
|
||||
/// `Err` is returned if the command could not be executed,
|
||||
/// **not** if the command returned a non-zero exit code.
|
||||
async fn execute(&self, args: &[&str]) -> Result<(usize, String, String), Self::Error>;
|
||||
async fn execute_raw(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error>;
|
||||
|
||||
/// Executes the given Git command with sane defaults.
|
||||
/// `git` is never passed as the first argument (arg 0).
|
||||
///
|
||||
/// Implementers should use this method over [`Self::execute_raw`]
|
||||
/// when possible.
|
||||
async fn execute(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut args = args.as_ref().to_vec();
|
||||
|
||||
args.insert(0, "--no-pager");
|
||||
// TODO(qix-): Test the performance impact of this.
|
||||
args.insert(0, "--no-optional-locks");
|
||||
|
||||
let mut envs = envs.unwrap_or_default();
|
||||
envs.insert("GIT_TERMINAL_PROMPT".into(), "0".into());
|
||||
envs.insert("LC_ALL".into(), "C".into()); // Force English. We need this for parsing output.
|
||||
|
||||
self.execute_raw(&args, Some(envs)).await
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
//! A [Tokio](https://tokio.rs)-based [`GitExecutor`] implementation.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::prelude::*;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// A [`GitExecutor`] implementation using the `git` command-line tool
|
||||
@ -10,9 +10,16 @@ pub struct TokioExecutor;
|
||||
impl super::GitExecutor for TokioExecutor {
|
||||
type Error = std::io::Error;
|
||||
|
||||
async fn execute(&self, args: &[&str]) -> Result<(usize, String, String), Self::Error> {
|
||||
async fn execute_raw(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args(args);
|
||||
if let Some(envs) = envs {
|
||||
cmd.envs(envs);
|
||||
}
|
||||
|
||||
let output = cmd.output().await?;
|
||||
|
||||
@ -23,82 +30,3 @@ impl super::GitExecutor for TokioExecutor {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`GitExecutor`] implementation using the `git` command-line tool
|
||||
/// via [`tokio::process::Command`], with the given environment variables.
|
||||
pub struct TokioExecutorEnv {
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl super::GitExecutor for TokioExecutorEnv {
|
||||
type Error = std::io::Error;
|
||||
|
||||
async fn execute(&self, args: &[&str]) -> Result<(usize, String, String), Self::Error> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args(args);
|
||||
cmd.envs(&self.env);
|
||||
|
||||
let output = cmd.output().await?;
|
||||
|
||||
Ok((
|
||||
output.status.code().unwrap_or(127) as usize,
|
||||
String::from_utf8_lossy(&output.stdout).trim().into(),
|
||||
String::from_utf8_lossy(&output.stderr).trim().into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows executors to create (or modify) a [`TokioExecutorEnv`],
|
||||
/// with added/modified environment variables, set for each execution
|
||||
/// of `git`.
|
||||
pub trait WithEnv: Sized {
|
||||
/// Sets the given environment variable.
|
||||
fn with_env<K: AsRef<str>, V: AsRef<str>>(self, key: K, value: V) -> TokioExecutorEnv;
|
||||
|
||||
/// Creates a new [`TokioExecutorEnv`] with the given additional environment variables.
|
||||
fn with_envs<K: AsRef<str>, V: AsRef<str>, I: IntoIterator<Item = (K, V)>>(
|
||||
self,
|
||||
envs: I,
|
||||
) -> TokioExecutorEnv;
|
||||
}
|
||||
|
||||
impl WithEnv for TokioExecutor {
|
||||
fn with_env<K: AsRef<str>, V: AsRef<str>>(self, key: K, value: V) -> TokioExecutorEnv {
|
||||
TokioExecutorEnv {
|
||||
env: [(key.as_ref().into(), value.as_ref().into())]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_envs<K: AsRef<str>, V: AsRef<str>, I: IntoIterator<Item = (K, V)>>(
|
||||
self,
|
||||
envs: I,
|
||||
) -> TokioExecutorEnv {
|
||||
TokioExecutorEnv {
|
||||
env: envs
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.as_ref().into(), v.as_ref().into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WithEnv for TokioExecutorEnv {
|
||||
fn with_env<K: AsRef<str>, V: AsRef<str>>(mut self, key: K, value: V) -> TokioExecutorEnv {
|
||||
self.env.insert(key.as_ref().into(), value.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
fn with_envs<K: AsRef<str>, V: AsRef<str>, I: IntoIterator<Item = (K, V)>>(
|
||||
mut self,
|
||||
envs: I,
|
||||
) -> TokioExecutorEnv {
|
||||
self.env.extend(
|
||||
envs.into_iter()
|
||||
.map(|(k, v)| (k.as_ref().into(), v.as_ref().into())),
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
//! NOTE: Doesn't support `no_std` yet.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use super::executor::GitExecutor;
|
||||
use crate::ConfigScope;
|
||||
use crate::{prelude::*, ConfigScope};
|
||||
use rand::Rng;
|
||||
|
||||
/// The number of characters in the secret used for checking
|
||||
/// askpass invocations by ssh/git when connecting to our process.
|
||||
const ASKPASS_SECRET_LENGTH: usize = 24;
|
||||
|
||||
/// Higher level errors that can occur when interacting with the CLI.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@ -14,6 +15,8 @@ pub enum Error<E: core::error::Error + core::fmt::Debug + Send + Sync + 'static>
|
||||
"git command exited with non-zero exit code {0}: {1:?}\n\nSTDOUT:\n{2}\n\nSTDERR:\n{3}"
|
||||
)]
|
||||
Failed(usize, Vec<String>, String, String),
|
||||
#[error("failed to determine path to this executable: {0}")]
|
||||
NoSelfExe(std::io::Error),
|
||||
}
|
||||
|
||||
/// A [`crate::Repository`] implementation using the `git` CLI
|
||||
@ -29,20 +32,20 @@ impl<E: GitExecutor> Repository<E> {
|
||||
/// Note that this **does not** check if the repository exists,
|
||||
/// but assumes it does.
|
||||
#[inline]
|
||||
pub fn open_unchecked<P: AsRef<Path>>(exec: E, path: P) -> Self {
|
||||
pub fn open_unchecked<P: AsRef<str>>(exec: E, path: P) -> Self {
|
||||
Self {
|
||||
exec,
|
||||
path: path.as_ref().to_str().unwrap().to_string(),
|
||||
path: path.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// (Re-)initializes a repository at the given path
|
||||
/// using the given [`GitExecutor`].
|
||||
pub async fn open_or_init<P: AsRef<Path>>(exec: E, path: P) -> Result<Self, Error<E::Error>> {
|
||||
let path = path.as_ref().to_str().unwrap().to_string();
|
||||
pub async fn open_or_init<P: AsRef<str>>(exec: E, path: P) -> Result<Self, Error<E::Error>> {
|
||||
let path = path.as_ref().to_owned();
|
||||
let args = vec!["init", "--quiet", &path];
|
||||
|
||||
let (exit_code, stdout, stderr) = exec.execute(&args).await.map_err(Error::Exec)?;
|
||||
let (exit_code, stdout, stderr) = exec.execute(&args, None).await.map_err(Error::Exec)?;
|
||||
|
||||
if exit_code == 0 {
|
||||
Ok(Self { exec, path })
|
||||
@ -55,6 +58,65 @@ impl<E: GitExecutor> Repository<E> {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_with_auth_harness(
|
||||
&self,
|
||||
args: &[&str],
|
||||
envs: Option<BTreeMap<String, String>>,
|
||||
) -> Result<(usize, String, String), Error<E::Error>> {
|
||||
let path = std::env::current_exe().map_err(|e| Error::NoSelfExe(e))?;
|
||||
let our_pid = std::process::id();
|
||||
|
||||
let askpath_path = path.with_file_name("gitbutler-git-askpass");
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let setsid_path = path.with_file_name("gitbutler-git-setsid");
|
||||
|
||||
let sock_path = std::env::temp_dir().join(format!("gitbutler-git-{our_pid}.sock"));
|
||||
|
||||
// FIXME(qix-): This is probably not cryptographically secure, did this in a bit
|
||||
// FIXME(qix-): of a hurry. We should probably use a proper CSPRNG here, but this
|
||||
// FIXME(qix-): is probably fine for now (as this security mechanism is probably
|
||||
// FIXME(qix-): overkill to begin with).
|
||||
let secret = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(ASKPASS_SECRET_LENGTH)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
|
||||
let mut envs = envs.unwrap_or_default();
|
||||
envs.insert(
|
||||
"GITBUTLER_ASKPASS_PIPE".into(),
|
||||
sock_path.to_string_lossy().into_owned(),
|
||||
);
|
||||
envs.insert("GITBUTLER_ASKPASS_SECRET".into(), secret.clone());
|
||||
envs.insert(
|
||||
"SSH_ASKPASS".into(),
|
||||
askpath_path.to_string_lossy().into_owned(),
|
||||
);
|
||||
|
||||
// DISPLAY is required by SSH to check SSH_ASKPASS.
|
||||
// Please don't ask us why, it's unclear.
|
||||
if !std::env::var("DISPLAY").map(|v| v != "").unwrap_or(false) {
|
||||
envs.insert("DISPLAY".into(), ":".into());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
envs.insert(
|
||||
"GIT_SSH_COMMAND".into(),
|
||||
format!(
|
||||
"{} {}",
|
||||
setsid_path.to_string_lossy(),
|
||||
envs.get("GIT_SSH_COMMAND").unwrap_or(&"ssh".into())
|
||||
),
|
||||
);
|
||||
|
||||
// TODO(qix-): implement the actual socket server code (right now this won't work)
|
||||
|
||||
self.exec
|
||||
.execute(args, Some(envs))
|
||||
.await
|
||||
.map_err(Error::Exec)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
@ -81,7 +143,8 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
|
||||
args.push(key);
|
||||
|
||||
let (exit_code, stdout, stderr) = self.exec.execute(&args).await.map_err(Error::Exec)?;
|
||||
let (exit_code, stdout, stderr) =
|
||||
self.exec.execute(&args, None).await.map_err(Error::Exec)?;
|
||||
|
||||
if exit_code == 0 {
|
||||
Ok(Some(stdout))
|
||||
@ -120,7 +183,8 @@ impl<E: GitExecutor + 'static> crate::Repository for Repository<E> {
|
||||
args.push(key);
|
||||
args.push(value);
|
||||
|
||||
let (exit_code, stdout, stderr) = self.exec.execute(&args).await.map_err(Error::Exec)?;
|
||||
let (exit_code, stdout, stderr) =
|
||||
self.exec.execute(&args, None).await.map_err(Error::Exec)?;
|
||||
|
||||
if exit_code == 0 {
|
||||
Ok(())
|
||||
|
@ -3,6 +3,26 @@
|
||||
//! This library houses a number of Git implementations,
|
||||
//! over which we abstract a common interface and provide
|
||||
//! higher-level operations that are implementation-agnostic.
|
||||
//!
|
||||
//! # Libgit2 Support
|
||||
//! This library supports libgit2 via the `git2` feature.
|
||||
//! Not much in the way of assumptions are made about the environment;
|
||||
//! it's a fairly clean and safe Git backend.
|
||||
//!
|
||||
//! # Fork/Exec (CLI) Support
|
||||
//! This library supports the Git CLI via the `cli` feature.
|
||||
//! Note that this is a fairly experimental implementation that
|
||||
//! uses some (ideally portable) hacks for authentication,
|
||||
//! including a custom executable (or two, in the case of
|
||||
//! *nix systems) for handling automatic authentication
|
||||
//! via the API.
|
||||
//!
|
||||
//! This means those executables must be situated next to
|
||||
//! the executable that is running them (as sibling files),
|
||||
//! for security purposes. They may not be symlinked.
|
||||
//!
|
||||
//! This hampers certain use cases, such as implementing
|
||||
//! [`cli::GitExecutor`] for e.g. remote connections.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)] // must be first
|
||||
#![feature(error_in_core)]
|
||||
|
@ -5,3 +5,7 @@ pub use alloc::{
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[allow(unused_imports)]
|
||||
pub use std::collections::BTreeMap;
|
||||
|
@ -1,7 +1,8 @@
|
||||
use core::fmt;
|
||||
|
||||
/// An error that can occur while parsing a refspec from a string.
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
pub enum Error {
|
||||
/// Encountered an unexpected character when parsing a [`RefSpec`] from a string.
|
||||
#[error("unexpected character {0:?} (offset {1})")]
|
||||
|
9
gitbutler-git/src/remote.rs
Normal file
9
gitbutler-git/src/remote.rs
Normal file
@ -0,0 +1,9 @@
|
||||
pub enum Authorization {
|
||||
Basic {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
},
|
||||
PublicKey {
|
||||
pub path: PathBuf
|
||||
}
|
||||
}
|
@ -18,7 +18,8 @@ import {
|
||||
catchError,
|
||||
of,
|
||||
startWith,
|
||||
combineLatestWith
|
||||
combineLatestWith,
|
||||
tap
|
||||
} from 'rxjs';
|
||||
|
||||
export type Update = { version?: string; status?: UpdateStatus } | undefined;
|
||||
@ -42,6 +43,7 @@ export class UpdaterService {
|
||||
|
||||
this.update$ = this.reload$.pipe(
|
||||
switchMap(() => interval(60 * 1000).pipe(startWith(0))),
|
||||
tap(() => this.status$.next(undefined)),
|
||||
switchMap(() =>
|
||||
from(checkUpdate()).pipe(
|
||||
timeout(10000), // In dev mode the promise hangs indefinitely.
|
||||
@ -61,7 +63,7 @@ export class UpdaterService {
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
// this.update$ = of({ version: '1.0.0', status: 'DONE' });
|
||||
this.update$ = of({ version: '1.0.0', status: 'UPTODATE' });
|
||||
}
|
||||
|
||||
async install() {
|
||||
|
@ -2,7 +2,7 @@ import type { PullRequest } from '$lib/github/types';
|
||||
import type { Branch, RemoteBranch } from '$lib/vbranches/types';
|
||||
import { CombinedBranch } from '$lib/branches/types';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { startWith, switchMap } from 'rxjs/operators';
|
||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||
import type { RemoteBranchService } from '$lib/stores/remoteBranches';
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import type { VirtualBranchService } from '$lib/vbranches/branchStoresCache';
|
||||
@ -30,7 +30,8 @@ export class BranchService {
|
||||
);
|
||||
observer.next(contributions);
|
||||
})
|
||||
)
|
||||
),
|
||||
map((branches) => branches.filter((b) => !b.vbranch || b.vbranch.active))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -57,11 +57,11 @@
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.viewport {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
.contents {
|
||||
display: block;
|
||||
|
@ -10,7 +10,7 @@
|
||||
$: update$ = updaterService.update$;
|
||||
</script>
|
||||
|
||||
{#if $update$?.version}
|
||||
{#if $update$?.version && $update$.status != 'UPTODATE'}
|
||||
<div class="update-banner" class:busy={$update$?.status == 'PENDING'}>
|
||||
<div class="img">
|
||||
<div class="circle-img">
|
||||
|
@ -17,7 +17,7 @@
|
||||
$: githubService = data.githubService;
|
||||
|
||||
$: project$ = data.project$;
|
||||
$: activeBranches$ = vbranchService.activeBranches$;
|
||||
$: branches = vbranchService.branches$;
|
||||
$: error$ = vbranchService.branchesError$;
|
||||
$: githubEnabled$ = githubService.isEnabled$;
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
project={$project$}
|
||||
{cloud}
|
||||
base={$base$}
|
||||
branches={$activeBranches$}
|
||||
branches={$branches}
|
||||
projectPath={$project$?.path}
|
||||
branchesError={$error$}
|
||||
user={$user$}
|
||||
@ -78,7 +78,6 @@
|
||||
overflow-x: scroll;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
.scroll-contents {
|
||||
display: flex;
|
||||
|
@ -115,6 +115,7 @@
|
||||
{projectPath}
|
||||
{user}
|
||||
{githubService}
|
||||
readonly={!branch.active}
|
||||
></BranchLane>
|
||||
</div>
|
||||
{/each}
|
||||
@ -205,8 +206,7 @@
|
||||
flex-shrink: 1;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
/* padding: 0 var(--space-8); */
|
||||
user-select: none;
|
||||
padding: 0 var(--space-8);
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
@ -173,13 +173,15 @@
|
||||
hover: 'cherrypick-dz-hover',
|
||||
active: 'cherrypick-dz-active',
|
||||
accepts: acceptCherrypick,
|
||||
onDrop: onCherrypicked
|
||||
onDrop: onCherrypicked,
|
||||
disabled: readonly
|
||||
}}
|
||||
use:dropzone={{
|
||||
hover: 'lane-dz-hover',
|
||||
active: 'lane-dz-active',
|
||||
accepts: acceptBranchDrop,
|
||||
onDrop: onBranchDrop
|
||||
onDrop: onBranchDrop,
|
||||
disabled: readonly
|
||||
}}
|
||||
>
|
||||
<DropzoneOverlay class="cherrypick-dz-marker" label="Apply here" />
|
||||
|
@ -63,7 +63,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="branch-files">
|
||||
<div class="branch-files" class:readonly>
|
||||
<div class="header" bind:this={headerElement}>
|
||||
<div class="header__left">
|
||||
{#if showCheckboxes && selectedListMode == 'list' && branch.files.length > 1}
|
||||
@ -115,6 +115,9 @@
|
||||
flex: 1;
|
||||
background: var(--clr-theme-container-light);
|
||||
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||
&.readonly {
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
}
|
||||
.scroll-container {
|
||||
display: flex;
|
||||
|
@ -10,6 +10,8 @@
|
||||
import type { GitHubService } from '$lib/github/service';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
|
||||
export let readonly = false;
|
||||
export let branch: Branch;
|
||||
@ -24,6 +26,7 @@
|
||||
let meatballButton: HTMLDivElement;
|
||||
let visible = false;
|
||||
let container: HTMLDivElement;
|
||||
let isApplying = false;
|
||||
|
||||
function handleBranchNameChange() {
|
||||
branchController.updateBranchName(branch.id, branch.name);
|
||||
@ -36,23 +39,115 @@
|
||||
$: hasIntegratedCommits = branch.commits?.some((b) => b.isIntegrated);
|
||||
</script>
|
||||
|
||||
<!-- <div class="header__wrapper"> -->
|
||||
<div class="header card" bind:this={container}>
|
||||
{#if !readonly}
|
||||
<div class="header__wrapper">
|
||||
<div class="header card" bind:this={container} class:readonly>
|
||||
<div class="header__info">
|
||||
<div class="header__label">
|
||||
<BranchLabel
|
||||
bind:name={branch.name}
|
||||
on:change={handleBranchNameChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
<div class="header__remote-branch">
|
||||
{#if !branch.upstream}
|
||||
{#if !branch.active}
|
||||
<Tooltip label="These changes are stashed away. Apply the lane to bring them back.">
|
||||
<div class="status-tag text-base-11 text-semibold unapplied">
|
||||
<Icon name="removed-branch-small" /> unapplied
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else if hasIntegratedCommits}
|
||||
<Tooltip
|
||||
label="These changes have been integrated upstream, update your applied branches to make this lane disappear."
|
||||
>
|
||||
<div class="status-tag text-base-11 text-semibold integrated">
|
||||
<Icon name="removed-branch-small" /> integrated
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<Tooltip label="These changes are in a virtual branch.">
|
||||
<div class="status-tag text-base-11 text-semibold pending">
|
||||
<Icon name="virtual-branch-small" /> virtual
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if !readonly}
|
||||
<div class="pending-name">
|
||||
<Tooltip
|
||||
label="Branch name that will be used when pushing. You can change it from the lane menu."
|
||||
>
|
||||
<span class="text-base-11 text-semibold">
|
||||
origin/{branch.upstreamName
|
||||
? branch.upstreamName
|
||||
: normalizeBranchName(branch.name)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Tooltip label="At least some of your changes have been pushed">
|
||||
<div class="status-tag text-base-11 text-semibold remote">
|
||||
<Icon name="remote-branch-small" /> remote
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tag
|
||||
icon="open-link"
|
||||
color="ghost"
|
||||
border
|
||||
clickable
|
||||
shrinkable
|
||||
on:click={(e) => {
|
||||
const url = branchUrl(base, branch.upstream?.name);
|
||||
if (url) open(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
origin/{branch.upstreamName}
|
||||
</Tag>
|
||||
{#if $pr$?.htmlUrl}
|
||||
<Tag
|
||||
icon="pr-small"
|
||||
color="ghost"
|
||||
border
|
||||
clickable
|
||||
on:click={(e) => {
|
||||
const url = $pr$?.htmlUrl;
|
||||
if (url) open(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
View PR
|
||||
</Tag>
|
||||
{/if}
|
||||
{/if}
|
||||
{#await branch.isMergeable then isMergeable}
|
||||
{#if !isMergeable}
|
||||
<Tooltip
|
||||
timeoutMilliseconds={100}
|
||||
label="Applying this branch will add merge conflict markers that you will have to resolve"
|
||||
>
|
||||
<Tag icon="locked-small" color="warning">Conflict</Tag>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
<div class="draggable" data-drag-handle>
|
||||
<Icon name="draggable-narrow" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__actions">
|
||||
<div class="header__buttons">
|
||||
{#if !readonly}
|
||||
<div class="draggable" data-drag-handle>
|
||||
<Icon name="draggable-narrow" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if branch.selectedForChanges}
|
||||
<Button icon="target" notClickable>Target branch</Button>
|
||||
<Button icon="target" notClickable disabled={readonly}>Target branch</Button>
|
||||
{:else}
|
||||
<Button
|
||||
icon="target"
|
||||
kind="outlined"
|
||||
color="neutral"
|
||||
disabled={readonly}
|
||||
on:click={async () => {
|
||||
await branchController.setSelectedForChanges(branch.id);
|
||||
}}
|
||||
@ -60,106 +155,88 @@
|
||||
Make target
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="header__buttons">
|
||||
<div class="relative" bind:this={meatballButton}>
|
||||
{#if !readonly}
|
||||
<Button
|
||||
icon="kebab"
|
||||
icon="cross-small"
|
||||
color="primary"
|
||||
kind="outlined"
|
||||
color="neutral"
|
||||
on:click={() => (visible = !visible)}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="branch-popup-menu"
|
||||
use:clickOutside={{
|
||||
trigger: meatballButton,
|
||||
handler: () => (visible = false)
|
||||
loading={isApplying}
|
||||
on:click={async () => {
|
||||
isApplying = true;
|
||||
try {
|
||||
await branchController.unapplyBranch(branch.id);
|
||||
} catch (e) {
|
||||
const err = 'Failed to apply branch';
|
||||
toast.error(err);
|
||||
console.error(err, e);
|
||||
} finally {
|
||||
isApplying = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BranchLanePopupMenu {branchController} {branch} {projectId} bind:visible on:action />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="header__info">
|
||||
<div class="header__label">
|
||||
<BranchLabel bind:name={branch.name} on:change={handleBranchNameChange} />
|
||||
</div>
|
||||
<div class="header__remote-branch">
|
||||
{#if !branch.upstream}
|
||||
{#if hasIntegratedCommits}
|
||||
<div class="status-tag text-base-11 text-semibold integrated">
|
||||
<Icon name="pr-small" /> integrated
|
||||
</div>
|
||||
Unapply
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="status-tag text-base-11 text-semibold pending">
|
||||
<Icon name="virtual-branch-small" /> virtual
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pending-name">
|
||||
<span class="text-base-11 text-semibold"
|
||||
>origin/{branch.upstreamName
|
||||
? branch.upstreamName
|
||||
: normalizeBranchName(branch.name)}</span
|
||||
<Button
|
||||
icon="plus-small"
|
||||
color="primary"
|
||||
kind="outlined"
|
||||
loading={isApplying}
|
||||
on:click={async () => {
|
||||
isApplying = true;
|
||||
try {
|
||||
await branchController.applyBranch(branch.id);
|
||||
} catch (e) {
|
||||
const err = 'Failed to apply branch';
|
||||
toast.error(err);
|
||||
console.error(err, e);
|
||||
} finally {
|
||||
isApplying = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="status-tag text-base-11 text-semibold remote">
|
||||
<Icon name="remote-branch-small" /> remote
|
||||
</div>
|
||||
<Tag
|
||||
icon="open-link"
|
||||
color="ghost"
|
||||
border
|
||||
clickable
|
||||
shrinkable
|
||||
on:click={(e) => {
|
||||
const url = branchUrl(base, branch.upstream?.name);
|
||||
if (url) open(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
Apply
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="relative" bind:this={meatballButton}>
|
||||
<Button
|
||||
icon="kebab"
|
||||
kind="outlined"
|
||||
color="neutral"
|
||||
on:click={() => (visible = !visible)}
|
||||
/>
|
||||
<div
|
||||
class="branch-popup-menu"
|
||||
use:clickOutside={{
|
||||
trigger: meatballButton,
|
||||
handler: () => (visible = false)
|
||||
}}
|
||||
>
|
||||
origin/{branch.upstreamName}
|
||||
</Tag>
|
||||
{#if $pr$?.htmlUrl}
|
||||
<Tag
|
||||
icon="pr-small"
|
||||
color="ghost"
|
||||
border
|
||||
clickable
|
||||
on:click={(e) => {
|
||||
const url = $pr$?.htmlUrl;
|
||||
if (url) open(url);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
View PR
|
||||
</Tag>
|
||||
{/if}
|
||||
{/if}
|
||||
<BranchLanePopupMenu
|
||||
{branchController}
|
||||
{branch}
|
||||
{projectId}
|
||||
{readonly}
|
||||
bind:visible
|
||||
on:action
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__top-overlay" data-remove-from-draggable data-tauri-drag-region />
|
||||
</div>
|
||||
|
||||
<!-- <div class="header__top-overlay" data-remove-from-draggable data-tauri-drag-region />
|
||||
</div> -->
|
||||
|
||||
<style lang="postcss">
|
||||
.header__wrapper {
|
||||
z-index: 10;
|
||||
position: sticky;
|
||||
top: var(--space-8);
|
||||
top: var(--space-16);
|
||||
}
|
||||
.header {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
@ -168,6 +245,9 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&.readonly {
|
||||
background: var(--clr-theme-container-pale);
|
||||
}
|
||||
}
|
||||
.header__top-overlay {
|
||||
z-index: 1;
|
||||
@ -176,15 +256,14 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--space-20);
|
||||
background: var(--target-branch-background);
|
||||
background: var(--clr-theme-container-pale);
|
||||
/* background-color: red; */
|
||||
}
|
||||
.header__info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin var(--transition-slow);
|
||||
padding: var(--space-12) var(--space-12) var(--space-16) var(--space-12);
|
||||
padding: var(--space-12);
|
||||
gap: var(--space-10);
|
||||
}
|
||||
.header__actions {
|
||||
@ -193,11 +272,14 @@
|
||||
background: var(--clr-theme-container-pale);
|
||||
padding: var(--space-12);
|
||||
justify-content: space-between;
|
||||
border-radius: var(--radius-m) var(--radius-m) 0 0;
|
||||
border-radius: 0 0 var(--radius-m) var(--radius-m);
|
||||
user-select: none;
|
||||
}
|
||||
.readonly .header__actions {
|
||||
background: var(--clr-theme-container-dim);
|
||||
}
|
||||
.header__buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
@ -208,11 +290,10 @@
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.draggable {
|
||||
/* position: absolute; */
|
||||
/* right: var(--space-4);
|
||||
top: var(--space-6); */
|
||||
/* opacity: 0; */
|
||||
margin-right: var(--space-4);
|
||||
position: absolute;
|
||||
right: var(--space-4);
|
||||
top: var(--space-6);
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
cursor: grab;
|
||||
color: var(--clr-theme-scale-ntrl-50);
|
||||
@ -291,4 +372,9 @@
|
||||
color: var(--clr-theme-scale-ntrl-100);
|
||||
background: var(--clr-theme-scale-ntrl-40);
|
||||
}
|
||||
|
||||
.unapplied {
|
||||
color: var(--clr-theme-scale-ntrl-30);
|
||||
background: var(--clr-theme-scale-ntrl-80);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
export let name: string;
|
||||
export let disabled = false;
|
||||
let inputActive = false;
|
||||
let label: HTMLDivElement;
|
||||
let input: HTMLInputElement;
|
||||
|
||||
function activateInput() {
|
||||
if (disabled) return;
|
||||
inputActive = true;
|
||||
setTimeout(() => input.select(), 0);
|
||||
}
|
||||
@ -25,6 +27,7 @@
|
||||
<span class="branch-name-mesure-el text-base-13" bind:this={mesureEl}>{name}</span>
|
||||
<input
|
||||
type="text"
|
||||
{disabled}
|
||||
bind:this={input}
|
||||
bind:value={name}
|
||||
on:change
|
||||
|
@ -49,26 +49,25 @@
|
||||
{user}
|
||||
{selectedFiles}
|
||||
{githubService}
|
||||
>
|
||||
<svelte:fragment slot="file-view">
|
||||
{#if selected}
|
||||
<FileCard
|
||||
conflicted={selected.conflicted}
|
||||
branchId={branch.id}
|
||||
file={selected}
|
||||
projectId={project.id}
|
||||
{projectPath}
|
||||
{branchController}
|
||||
{selectedOwnership}
|
||||
selectable={$commitBoxOpen && !readonly}
|
||||
on:close={() => {
|
||||
const selectedId = selected?.id;
|
||||
selectedFiles.update((fileIds) => fileIds.filter((file) => file.id != selectedId));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</BranchCard>
|
||||
/>
|
||||
|
||||
{#if selected}
|
||||
<FileCard
|
||||
conflicted={selected.conflicted}
|
||||
branchId={branch.id}
|
||||
file={selected}
|
||||
projectId={project.id}
|
||||
{projectPath}
|
||||
{branchController}
|
||||
{selectedOwnership}
|
||||
{readonly}
|
||||
selectable={$commitBoxOpen && !readonly}
|
||||
on:close={() => {
|
||||
const selectedId = selected?.id;
|
||||
selectedFiles.update((fileIds) => fileIds.filter((file) => file.id != selectedId));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
|
@ -14,6 +14,7 @@
|
||||
export let branch: Branch;
|
||||
export let projectId: string;
|
||||
export let visible: boolean;
|
||||
export let readonly = false;
|
||||
|
||||
let deleteBranchModal: Modal;
|
||||
let renameRemoteModal: Modal;
|
||||
@ -33,10 +34,15 @@
|
||||
{#if visible}
|
||||
<ContextMenu>
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Unapply"
|
||||
on:click={() => branch.id && branchController.unapplyBranch(branch.id)}
|
||||
/>
|
||||
{#if !readonly}
|
||||
<ContextMenuItem
|
||||
label="Unapply"
|
||||
on:click={() => {
|
||||
if (branch.id) branchController.unapplyBranch(branch.id);
|
||||
visible = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ContextMenuItem
|
||||
label="Delete"
|
||||
@ -52,13 +58,13 @@
|
||||
dispatch('action', 'generate-branch-name');
|
||||
visible = false;
|
||||
}}
|
||||
disabled={!$aiGenEnabled || branch.files?.length == 0 || !branch.active}
|
||||
disabled={readonly || !$aiGenEnabled || branch.files?.length == 0 || !branch.active}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Set branch name"
|
||||
disabled={hasIntegratedCommits}
|
||||
disabled={readonly || hasIntegratedCommits}
|
||||
on:click={() => {
|
||||
newRemoteName = branch.upstreamName || '';
|
||||
visible = false;
|
||||
|
@ -27,7 +27,7 @@
|
||||
export let conflicted: boolean;
|
||||
export let projectPath: string | undefined;
|
||||
export let branchController: BranchController;
|
||||
export let readonly = false;
|
||||
export let readonly: boolean;
|
||||
export let selectable = false;
|
||||
export let selectedOwnership: Writable<Ownership>;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user