Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mod project_git;
mod project_git_branches;
mod project_git_diff;
mod project_git_exec;
mod project_git_focus;
mod project_git_merge_error;
mod project_git_push;
mod project_git_workflow;
Expand Down
45 changes: 28 additions & 17 deletions desktop/src-tauri/src/commands/project_git.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use super::project_git_exec::{
build_git_auth_config, clean_branch, clean_target_ref, run_git, validate_workspace_clone_url,
GitAuthConfig,
build_git_auth_config, clean_branch, run_git, validate_workspace_clone_url, GitAuthConfig,
};
use super::project_git_focus::{
clean_repository_target, select_repository_tree_lines, RepositoryTarget,
};
use super::project_git_push::push_project_local_repository_blocking;
use super::project_repo_paths::{canonical_repos_roots, find_local_repo_dir};
Expand Down Expand Up @@ -313,9 +315,10 @@ fn parse_ls_tree(
repo_dir: &std::path::Path,
output: &str,
latest_commit_by_path: &std::collections::HashMap<String, ProjectRepoCommitInfo>,
focus_path: Option<&str>,
) -> Vec<ProjectRepoFileInfo> {
output
.lines()
select_repository_tree_lines(output, focus_path, 250, 250)
.into_iter()
.filter_map(|line| {
let (meta, path) = line.split_once('\t')?;
let mut parts = meta.split_whitespace();
Expand All @@ -339,7 +342,6 @@ fn parse_ls_tree(
latest_commit: latest_commit_by_path.get(path).cloned(),
})
})
.take(250)
.collect()
}

Expand All @@ -348,6 +350,7 @@ fn snapshot_from_repo(
auth: &GitAuthConfig,
branch_name: Option<&str>,
base_branch: Option<&str>,
focus_path: Option<&str>,
) -> ProjectRepoSnapshotInfo {
let latest_commit = run_git(
&["log", "-1", "--format=%H%x00%h%x00%an%x00%ae%x00%at%x00%s"],
Expand Down Expand Up @@ -397,10 +400,13 @@ fn snapshot_from_repo(
)
.map(|output| parse_latest_commit_by_path(&output))
.unwrap_or_default();

run_git(&["ls-tree", "-r", "--long", "HEAD"], Some(repo_dir), auth)
.map(|output| parse_ls_tree(repo_dir, &output, &latest_commit_by_path))
.unwrap_or_default()
run_git(
&["ls-tree", "-r", "--long", "-z", "HEAD"],
Some(repo_dir),
auth,
)
.map(|output| parse_ls_tree(repo_dir, &output, &latest_commit_by_path, focus_path))
.unwrap_or_default()
} else {
Vec::new()
};
Expand Down Expand Up @@ -712,18 +718,18 @@ pub async fn get_project_repo_snapshot(
base_branch: Option<String>,
target_ref: Option<String>,
target_commit: Option<String>,
target_path: Option<String>,
state: State<'_, AppState>,
) -> Result<ProjectRepoSnapshotInfo, String> {
validate_workspace_clone_url(&clone_url, &state)?;
let auth = build_git_auth_config(&state)?;
let branch = clean_branch(default_branch);
let base_branch = clean_branch(base_branch);
let target_ref = clean_target_ref(target_ref);
let target_commit = target_commit
.map(|value| value.to_ascii_lowercase())
.filter(|value| matches!(value.len(), 40 | 64))
.filter(|value| value.chars().all(|c| c.is_ascii_hexdigit()));

let RepositoryTarget {
target_ref,
target_commit,
target_path,
} = clean_repository_target(target_ref, target_commit, target_path)?;
tauri::async_runtime::spawn_blocking(move || {
let temp_dir = tempfile::tempdir().map_err(|error| format!("create temp dir: {error}"))?;
let repo_dir = temp_dir.path().join("repo");
Expand Down Expand Up @@ -783,8 +789,13 @@ pub async fn get_project_repo_snapshot(
}
}

let snapshot =
snapshot_from_repo(&repo_dir, &auth, branch.as_deref(), base_branch.as_deref());
let snapshot = snapshot_from_repo(
&repo_dir,
&auth,
branch.as_deref(),
base_branch.as_deref(),
target_path.as_deref(),
);
Ok(snapshot)
})
.await
Expand Down
60 changes: 54 additions & 6 deletions desktop/src-tauri/src/commands/project_git_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,15 +267,31 @@ pub(crate) fn clean_branch(value: Option<String>) -> Option<String> {

pub(crate) fn clean_target_ref(value: Option<String>) -> Option<String> {
let value = value?.trim().to_string();
for prefix in ["refs/tags/", "refs/nostr/"] {
for prefix in ["refs/heads/", "refs/tags/", "refs/nostr/"] {
if let Some(name) = value.strip_prefix(prefix) {
let clean_name = clean_branch(Some(name.to_string()))?;
if prefix == "refs/heads/"
&& (clean_name.ends_with('.')
|| clean_name.ends_with(".lock")
|| clean_name.contains("//")
|| clean_name
.split('/')
.any(|component| component.starts_with('.')))
{
return None;
}
return (clean_name == name).then_some(format!("{prefix}{clean_name}"));
}
}
None
}

pub(crate) fn clean_target_commit(value: Option<String>) -> Option<String> {
let value = value?.trim().to_ascii_lowercase();
(matches!(value.len(), 40 | 64) && value.chars().all(|c| c.is_ascii_hexdigit()))
.then_some(value)
}

pub(crate) fn validate_clone_url(clone_url: &str) -> Result<(), String> {
let parsed = Url::parse(clone_url).map_err(|error| format!("invalid clone URL: {error}"))?;
if !matches!(parsed.scheme(), "http" | "https") {
Expand Down Expand Up @@ -393,9 +409,9 @@ fn validate_clone_url_against_relay(clone_url: &str, relay_base: &str) -> Result
#[cfg(test)]
mod tests {
use super::{
clean_branch, clean_target_ref, credential_helper_config_value, git_needs_credentials,
git_subcommand, validate_clone_url, validate_clone_url_against_relay,
validate_local_clone_url,
clean_branch, clean_target_commit, clean_target_ref, credential_helper_config_value,
git_needs_credentials, git_subcommand, validate_clone_url,
validate_clone_url_against_relay, validate_local_clone_url,
};

#[test]
Expand Down Expand Up @@ -463,7 +479,11 @@ mod tests {
}

#[test]
fn clean_target_ref_accepts_only_tags_and_pull_request_refs() {
fn clean_target_ref_accepts_heads_tags_and_pull_request_refs() {
assert_eq!(
clean_target_ref(Some("refs/heads/feature/x-1".into())),
Some("refs/heads/feature/x-1".to_string())
);
assert_eq!(
clean_target_ref(Some("refs/tags/v1.0.0".into())),
Some("refs/tags/v1.0.0".to_string())
Expand All @@ -472,10 +492,38 @@ mod tests {
clean_target_ref(Some("refs/nostr/abc123".into())),
Some("refs/nostr/abc123".to_string())
);
assert_eq!(clean_target_ref(Some("refs/heads/main".into())), None);
assert_eq!(clean_target_ref(Some("refs/heads/../main".into())), None);
assert_eq!(clean_target_ref(Some("refs/tags/../main".into())), None);
}

#[test]
fn clean_target_ref_rejects_noncanonical_head_names() {
for value in [
"refs/heads/main.",
"refs/heads/main.lock",
"refs/heads/feature//main",
"refs/heads/.hidden/main",
"refs/heads/feature/.hidden",
] {
assert_eq!(clean_target_ref(Some(value.into())), None, "{value}");
}
}

#[test]
fn clean_target_commit_accepts_full_hashes_and_rejects_other_values() {
assert_eq!(
clean_target_commit(Some(" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ".into())),
Some("a".repeat(40))
);
assert_eq!(
clean_target_commit(Some("B".repeat(64))),
Some("b".repeat(64))
);
for value in ["", "abc", &"g".repeat(40), &"a".repeat(39)] {
assert_eq!(clean_target_commit(Some(value.into())), None, "{value}");
}
}

#[test]
fn validate_clone_url_requires_buzz_repo_shape() {
let owner = "a".repeat(64);
Expand Down
Loading