merged upstream

This commit is contained in:
PavelLaptev 2023-11-24 12:16:26 +01:00 committed by GitButler
commit 44a032f492
18 changed files with 224 additions and 138 deletions

View File

@ -77,5 +77,4 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/init-env-rust - uses: ./.github/actions/init-env-rust
- run: cargo install cargo-cranky - run: cargo clippy --all-targets --all-features --tests
- run: cargo cranky --all-targets --all-features

View File

@ -10,3 +10,80 @@ codegen-units = 1 # Compile crates one after another so the compiler can optimiz
lto = true # Enables link to optimizations lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size opt-level = "s" # Optimize for binary size
debug = true # Enable debug symbols, for sentry debug = true # Enable debug symbols, for sentry
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
all = "deny"
perf = "deny"
correctness = "deny"
complexity = "deny"
style = "deny"
pedantic = "deny"
# selection from clippy::restriction (see https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=restriction)
as_underscore = "deny"
assertions_on_result_states = "deny"
clone_on_ref_ptr = "deny"
create_dir = "deny"
dbg_macro = "deny"
decimal_literal_representation = "deny"
default_numeric_fallback = "deny"
empty_drop = "deny"
empty_structs_with_brackets = "deny"
exit = "deny"
filetype_is_file = "deny"
float_cmp_const = "deny"
fn_to_numeric_cast_any = "deny"
format_push_string = "deny"
get_unwrap = "deny"
integer_division = "deny"
lossy_float_literal = "deny"
mem_forget = "deny"
mixed_read_write_in_expression = "deny"
mutex_atomic = "deny"
needless_raw_strings = "deny"
non_ascii_literal = "deny"
panic = "deny"
print_stderr = "deny"
pub_without_shorthand = "deny"
rc_buffer = "deny"
rc_mutex = "deny"
redundant_type_annotations = "deny"
ref_patterns = "deny"
rest_pat_in_fully_bound_structs = "deny"
same_name_method = "deny"
string_add = "deny"
string_lit_chars_any = "deny"
string_slice = "deny"
string_to_string = "deny"
suspicious_xor_used_as_pow = "deny"
todo = "deny"
try_err = "deny"
unimplemented = "deny"
unnecessary_self_imports = "deny"
unneeded_field_pattern = "deny"
unseparated_literal_suffix = "deny"
if_then_some_else_none = "deny"
use_debug = "deny"
# TODO
# partial_pub_fields = "deny"
# print_stdout = "deny"
# unwrap_used = "deny"
# unwrap_in_result = "deny"
# noise and or false-positives
missing_errors_doc = { level = "allow", priority = 1 }
used_underscore_binding = { level = "allow", priority = 1 }
must_use_candidate = { level = "allow", priority = 1 }
module_name_repetitions = { level = "allow", priority = 1 }
missing_panics_doc = { level = "allow", priority = 1 }
too_many_lines = { level = "allow", priority = 1 }
implicit_hasher = { level = "allow", priority = 1 }
if_not_else = { level = "allow", priority = 1 }
return_self_not_must_use = { level = "allow", priority = 1 }
inconsistent_struct_constructor = { level = "allow", priority = 1 }
match_wildcard_for_single_variants = { level = "allow", priority = 1 }
unnested_or_patterns = { level = "allow", priority = 1 }
similar_names = { level = "allow", priority = 1 }

View File

@ -1,78 +0,0 @@
# https://github.com/ericseppanen/cargo-cranky
# cargo install cargo-cranky && cargo cranky
deny = [
"clippy::all",
"clippy::perf",
"clippy::correctness",
"clippy::complexity",
"clippy::style",
"clippy::pedantic",
# selection from clippy::restriction (see https://rust-lang.github.io/rust-clippy/master/index.html#/?groups=restriction)
"clippy::as_underscore",
"clippy::assertions_on_result_states",
"clippy::clone_on_ref_ptr",
"clippy::create_dir",
"clippy::dbg_macro",
"clippy::decimal_literal_representation",
"clippy::default_numeric_fallback",
"clippy::empty_drop",
"clippy::empty_structs_with_brackets",
"clippy::exit",
"clippy::filetype_is_file",
"clippy::float_cmp_const",
"clippy::fn_to_numeric_cast_any",
"clippy::format_push_string",
"clippy::get_unwrap",
"clippy::integer_division",
"clippy::lossy_float_literal",
"clippy::mem_forget",
"clippy::mixed_read_write_in_expression",
"clippy::mutex_atomic",
"clippy::needless_raw_strings",
"clippy::non_ascii_literal",
"clippy::panic",
"clippy::print_stderr",
"clippy::pub_without_shorthand",
"clippy::rc_buffer",
"clippy::rc_mutex",
"clippy::redundant_type_annotations",
"clippy::ref_patterns",
"clippy::rest_pat_in_fully_bound_structs",
"clippy::same_name_method",
"clippy::string_add",
"clippy::string_lit_chars_any",
"clippy::string_slice",
"clippy::string_to_string",
"clippy::suspicious_xor_used_as_pow",
"clippy::todo",
"clippy::try_err",
"clippy::unimplemented",
"clippy::unnecessary_self_imports",
"clippy::unneeded_field_pattern",
"clippy::unseparated_literal_suffix",
"clippy::if_then_some_else_none",
"clippy::use_debug",
#TODO:
# "clippy::partial_pub_fields"
# "clippy::print_stdout"
# "clippy::unwrap_used"
# "clippy::unwrap_in_result"
]
allow = [
# reason = "noise and or false-positives"
"clippy::missing_errors_doc",
"clippy::used_underscore_binding",
"clippy::must_use_candidate",
"clippy::module_name_repetitions",
"clippy::missing_panics_doc",
"clippy::too_many_lines",
"clippy::implicit_hasher",
"clippy::if_not_else",
"clippy::return_self_not_must_use",
"clippy::inconsistent_struct_constructor",
"clippy::match_wildcard_for_single_variants",
"clippy::unnested_or_patterns",
"clippy::similar_names",
]

View File

@ -16,3 +16,6 @@ dialoguer = "0.11.0"
dirs = "5.0.1" dirs = "5.0.1"
git2 = { version = "0.18.1", features = ["vendored-openssl", "vendored-libgit2"] } git2 = { version = "0.18.1", features = ["vendored-openssl", "vendored-libgit2"] }
gitbutler = { path = "../tauri" } gitbutler = { path = "../tauri" }
[lints]
workspace = true

View File

@ -84,3 +84,6 @@ devtools = ["tauri/devtools"]
# this feature is used used for production builds where `devPath` points to the filesystem # this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this # DO NOT remove this
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[lints]
workspace = true

View File

@ -1,5 +1,3 @@
#![forbid(unsafe_code)]
use anyhow::Context; use anyhow::Context;
use tauri::{generate_context, Manager}; use tauri::{generate_context, Manager};

View File

@ -4,6 +4,8 @@ use crate::git;
pub enum Error { pub enum Error {
#[error("branch name is invalid: {0}")] #[error("branch name is invalid: {0}")]
InvalidName(String), InvalidName(String),
#[error("reference is not a tag: {0}")]
NotTag(String),
#[error("branch is not local: {0}")] #[error("branch is not local: {0}")]
NotLocal(String), NotLocal(String),
#[error("branch is not remote: {0}")] #[error("branch is not remote: {0}")]

View File

@ -1,6 +1,7 @@
mod error; mod error;
mod local; mod local;
mod remote; mod remote;
mod tag;
mod r#virtual; mod r#virtual;
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
@ -11,12 +12,15 @@ pub use error::Error;
pub use local::Refname as LocalRefname; pub use local::Refname as LocalRefname;
pub use r#virtual::Refname as VirtualRefname; pub use r#virtual::Refname as VirtualRefname;
pub use remote::Refname as RemoteRefname; pub use remote::Refname as RemoteRefname;
pub use tag::Refname as TagRefname;
use crate::git; use crate::git;
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Refname { pub enum Refname {
HEAD, HEAD,
STASH,
Tag(TagRefname),
Remote(RemoteRefname), Remote(RemoteRefname),
Local(LocalRefname), Local(LocalRefname),
Virtual(VirtualRefname), Virtual(VirtualRefname),
@ -59,12 +63,12 @@ impl From<&LocalRefname> for Refname {
} }
impl Refname { impl Refname {
pub fn branch(&self) -> &str { pub fn branch(&self) -> Option<&str> {
match self { match self {
Self::HEAD => "HEAD", Self::HEAD | Self::Tag(_) | Self::STASH => None,
Self::Remote(remote) => remote.branch(), Self::Remote(remote) => Some(remote.branch()),
Self::Local(local) => local.branch(), Self::Local(local) => Some(local.branch()),
Self::Virtual(r#virtual) => r#virtual.branch(), Self::Virtual(r#virtual) => Some(r#virtual.branch()),
} }
} }
} }
@ -73,20 +77,14 @@ impl FromStr for Refname {
type Err = Error; type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> { fn from_str(value: &str) -> Result<Self, Self::Err> {
if value == "HEAD" { match value {
Ok(Self::HEAD) "HEAD" => Ok(Self::HEAD),
} else if value.starts_with("refs") { "refs/stash" => Ok(Self::STASH),
if value.starts_with("refs/remotes/") { value if value.starts_with("refs/tags/") => Ok(Self::Tag(value.parse()?)),
Ok(Self::Remote(value.parse()?)) value if value.starts_with("refs/remotes/") => Ok(Self::Remote(value.parse()?)),
} else if value.starts_with("refs/heads/") { value if value.starts_with("refs/heads/") => Ok(Self::Local(value.parse()?)),
Ok(Self::Local(value.parse()?)) value if value.starts_with("refs/gitbutler/") => Ok(Self::Virtual(value.parse()?)),
} else if value.starts_with("refs/gitbutler/") { _ => Err(Error::InvalidName(value.to_string())),
Ok(Self::Virtual(value.parse()?))
} else {
Err(Error::InvalidName(value.to_string()))
}
} else {
Ok(Self::Local(value.parse()?))
} }
} }
} }
@ -107,6 +105,8 @@ impl fmt::Display for Refname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::HEAD => write!(f, "HEAD"), Self::HEAD => write!(f, "HEAD"),
Self::STASH => write!(f, "refs/stash"),
Self::Tag(tag) => tag.fmt(f),
Self::Remote(remote) => remote.fmt(f), Self::Remote(remote) => remote.fmt(f),
Self::Local(local) => local.fmt(f), Self::Local(local) => local.fmt(f),
Self::Virtual(r#virtual) => r#virtual.fmt(f), Self::Virtual(r#virtual) => r#virtual.fmt(f),
@ -117,7 +117,9 @@ impl fmt::Display for Refname {
impl Serialize for Refname { impl Serialize for Refname {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self { match self {
Self::STASH => serializer.serialize_str("refs/stash"),
Self::HEAD => serializer.serialize_str("HEAD"), Self::HEAD => serializer.serialize_str("HEAD"),
Self::Tag(tag) => tag.serialize(serializer),
Self::Remote(remote) => remote.serialize(serializer), Self::Remote(remote) => remote.serialize(serializer),
Self::Local(local) => local.serialize(serializer), Self::Local(local) => local.serialize(serializer),
Self::Virtual(r#virtual) => r#virtual.serialize(serializer), Self::Virtual(r#virtual) => r#virtual.serialize(serializer),

View File

@ -0,0 +1,53 @@
use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize};
use super::error::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Refname {
tag: String,
}
impl Refname {
pub fn tag(&self) -> &str {
&self.tag
}
}
impl Serialize for Refname {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'d> Deserialize<'d> for Refname {
fn deserialize<D: serde::Deserializer<'d>>(deserializer: D) -> Result<Self, D::Error> {
let name = String::deserialize(deserializer)?;
name.as_str().parse().map_err(serde::de::Error::custom)
}
}
impl fmt::Display for Refname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "refs/tags/{}", self.tag)
}
}
impl FromStr for Refname {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if !value.starts_with("refs/tags/") {
return Err(Error::NotTag(value.to_string()));
}
if let Some(tag) = value.strip_prefix("refs/tags/") {
Ok(Self {
tag: tag.to_string(),
})
} else {
Err(Error::InvalidName(value.to_string()))
}
}
}

View File

@ -2,7 +2,7 @@ use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{git, virtual_branches::Branch}; use crate::virtual_branches::Branch;
use super::error::Error; use super::error::Error;
@ -62,16 +62,3 @@ impl FromStr for Refname {
} }
} }
} }
impl TryFrom<&git::Branch<'_>> for Refname {
type Error = Error;
fn try_from(value: &git::Branch<'_>) -> std::result::Result<Self, Self::Error> {
let branch_name = String::from_utf8(value.refname_bytes().to_vec()).map_err(Error::Utf8)?;
if value.is_remote() {
Err(Error::NotLocal(branch_name))
} else {
branch_name.parse()
}
}
}

View File

@ -330,6 +330,8 @@ impl Repository {
.find_branch( .find_branch(
&match name { &match name {
Refname::HEAD => "HEAD".to_string(), Refname::HEAD => "HEAD".to_string(),
Refname::STASH => "stash".to_string(),
Refname::Tag(tag) => tag.tag().to_string(),
Refname::Virtual(virtual_refname) => virtual_refname.branch().to_string(), Refname::Virtual(virtual_refname) => virtual_refname.branch().to_string(),
Refname::Local(local) => local.branch().to_string(), Refname::Local(local) => local.branch().to_string(),
Refname::Remote(remote) => { Refname::Remote(remote) => {
@ -337,9 +339,11 @@ impl Repository {
} }
}, },
match name { match name {
Refname::HEAD | Refname::Virtual(_) | Refname::Local(_) => { Refname::STASH
git2::BranchType::Local | Refname::HEAD
} | Refname::Virtual(_)
| Refname::Local(_)
| Refname::Tag(_) => git2::BranchType::Local,
Refname::Remote(_) => git2::BranchType::Remote, Refname::Remote(_) => git2::BranchType::Remote,
}, },
) )

View File

@ -1,5 +1,3 @@
#![forbid(unsafe_code)]
pub mod analytics; pub mod analytics;
pub mod app; pub mod app;
pub mod assets; pub mod assets;

View File

@ -580,6 +580,12 @@ pub fn create_virtual_branch_from_branch(
applied: Option<bool>, applied: Option<bool>,
user: Option<&users::User>, user: Option<&users::User>,
) -> Result<branch::Branch, CreateVirtualBranchFromBranchError> { ) -> Result<branch::Branch, CreateVirtualBranchFromBranchError> {
if !matches!(upstream, git::Refname::Local(_) | git::Refname::Remote(_)) {
return Err(errors::CreateVirtualBranchFromBranchError::BranchNotFound(
upstream.clone(),
));
}
let current_session = gb_repository let current_session = gb_repository
.get_or_create_current_session() .get_or_create_current_session()
.context("failed to get or create current session")?; .context("failed to get or create current session")?;
@ -623,8 +629,11 @@ pub fn create_virtual_branch_from_branch(
// only set upstream if it's not the default target // only set upstream if it's not the default target
let upstream_branch = match upstream { let upstream_branch = match upstream {
git::Refname::Virtual(_) | git::Refname::HEAD => { git::Refname::STASH
// we don't support creating virtual branches from virtual branches | git::Refname::Virtual(_)
| git::Refname::HEAD
| git::Refname::Tag(_) => {
// we only support local or remote branches
return Err(errors::CreateVirtualBranchFromBranchError::BranchNotFound( return Err(errors::CreateVirtualBranchFromBranchError::BranchNotFound(
upstream.clone(), upstream.clone(),
)); ));
@ -639,7 +648,10 @@ pub fn create_virtual_branch_from_branch(
let mut branch = branch::Branch { let mut branch = branch::Branch {
id: BranchId::generate(), id: BranchId::generate(),
name: upstream.branch().to_string(), name: upstream
.branch()
.expect("always a branch reference")
.to_string(),
notes: String::new(), notes: String::new(),
applied: applied.unwrap_or(false), applied: applied.unwrap_or(false),
upstream: upstream_branch, upstream: upstream_branch,

View File

@ -66,7 +66,7 @@ pub fn list_remote_branches(
.context("failed to convert branches")? .context("failed to convert branches")?
.into_iter() .into_iter()
.flatten() .flatten()
.filter(|branch| branch.name.branch() != default_target.branch.branch()) .filter(|branch| branch.name.branch() != Some(default_target.branch.branch()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(remote_branches) Ok(remote_branches)

View File

@ -5,7 +5,7 @@ use tauri::AppHandle;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
gb_repository, gb_repository, git,
paths::DataDir, paths::DataDir,
project_repository, project_repository,
projects::{self, CodePushState, ProjectId}, projects::{self, CodePushState, ProjectId},
@ -144,10 +144,16 @@ impl HandlerInner {
Err(err) => return Err(err).context("failed to push"), Err(err) => return Err(err).context("failed to push"),
}; };
let refs = gb_refs(&project_repository)?; let refnames = gb_refs(&project_repository)?;
let all_refs = refs let all_refs = refnames
.iter() .iter()
.filter(|r| {
matches!(
r,
git::Refname::Remote(_) | git::Refname::Virtual(_) | git::Refname::Local(_)
)
})
.map(|r| format!("+{}:{}", r, r)) .map(|r| format!("+{}:{}", r, r))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -169,11 +175,13 @@ impl HandlerInner {
} }
} }
fn gb_refs(project_repository: &project_repository::Repository) -> anyhow::Result<Vec<String>> { fn gb_refs(
project_repository: &project_repository::Repository,
) -> anyhow::Result<Vec<git::Refname>> {
Ok(project_repository Ok(project_repository
.git_repository .git_repository
.references_glob("refs/*")? .references_glob("refs/*")?
.flatten() .flatten()
.filter_map(|r| r.name().map(|name| name.to_string())) .filter_map(|r| r.name())
.collect::<Vec<_>>()) .collect::<Vec<_>>())
} }

View File

@ -109,9 +109,13 @@ impl TestProject {
/// works like if we'd open and merge a PR on github. does not update local. /// works like if we'd open and merge a PR on github. does not update local.
pub fn merge(&self, branch_name: &git::Refname) { pub fn merge(&self, branch_name: &git::Refname) {
let branch_name: git::Refname = format!("refs/heads/{}", branch_name.branch()) let branch_name: git::Refname = match branch_name {
.parse() git::Refname::Local(local) => format!("refs/heads/{}", local.branch()).parse().unwrap(),
.unwrap(); git::Refname::Remote(remote) => {
format!("refs/heads/{}", remote.branch()).parse().unwrap()
}
_ => "INVALID".parse().unwrap(), // todo
};
let branch = self.remote_repository.find_branch(&branch_name).unwrap(); let branch = self.remote_repository.find_branch(&branch_name).unwrap();
let branch_commit = branch.peel_to_commit().unwrap(); let branch_commit = branch.peel_to_commit().unwrap();

View File

@ -1,6 +1,6 @@
import { invoke } from '$lib/backend/ipc'; import { invoke } from '$lib/backend/ipc';
import type { Project as CloudProject } from '$lib/backend/cloud'; import type { Project as CloudProject } from '$lib/backend/cloud';
import { BehaviorSubject, catchError, from, switchMap } from 'rxjs'; import { BehaviorSubject, Observable, catchError, from, map, shareReplay, switchMap } from 'rxjs';
export type Key = export type Key =
| 'generated' | 'generated'
@ -22,6 +22,7 @@ export class ProjectService {
error$ = new BehaviorSubject<any>(undefined); error$ = new BehaviorSubject<any>(undefined);
projects$ = this.reload$.pipe( projects$ = this.reload$.pipe(
switchMap(() => from(invoke<Project[]>('list_projects'))), switchMap(() => from(invoke<Project[]>('list_projects'))),
shareReplay(1),
catchError((e) => { catchError((e) => {
this.error$.next(e); this.error$.next(e);
return []; return [];
@ -31,20 +32,33 @@ export class ProjectService {
constructor() {} constructor() {}
getProject(projectId: string) { getProject(projectId: string) {
return from(invoke<Project>('get_project', { id: projectId })); return this.projects$.pipe(
map((projects) => {
const project = projects.find((p) => (p.id = projectId));
if (!project) {
// We need to abort loading of /[projectId]/ if no project exists, such
// that the type here is of Project rather than Project | undefined.
throw 'Project not found';
}
return project;
})
);
} }
updateProject(params: { async updateProject(params: {
id: string; id: string;
title?: string; title?: string;
api?: CloudProject & { sync: boolean }; api?: CloudProject & { sync: boolean };
preferred_key?: Key; preferred_key?: Key;
}) { }) {
return invoke<Project>('update_project', { project: params }); await invoke<Project>('update_project', { project: params });
this.reload();
} }
async add(path: string) { async add(path: string) {
return await invoke<Project>('add_project', { path }); const project = await invoke<Project>('add_project', { path });
this.reload();
return project;
} }
async deleteProject(id: string) { async deleteProject(id: string) {

View File

@ -7,7 +7,7 @@ set -o pipefail
function rust() { function rust() {
cargo fmt --check cargo fmt --check
cargo sort -c -w cargo sort -c -w
cargo cranky --all-targets --all-features cargo clippy --all-targets --all-features --tests
cargo test cargo test
} }