-
Notifications
You must be signed in to change notification settings - Fork 1
Address PR review feedback: security, CLI ergonomics, git2 add_submodule, and correctness fixes #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -288,12 +288,92 @@ impl GitOperations for Git2Operations { | |
| Ok(()) | ||
| } | ||
| fn add_submodule(&mut self, opts: &SubmoduleAddOptions) -> Result<()> { | ||
| // git2 submodule cloning requires remote callbacks that are complex to configure. | ||
| // Fall through to the CLI fallback which handles this reliably. | ||
| Err(anyhow::anyhow!( | ||
| "Unable to add submodule '{}' using the library API; it will be added using the Git CLI instead", | ||
| opts.name | ||
| )) | ||
| // 1. Create submodule entry in .gitmodules and index | ||
| let mut sub = self | ||
| .repo | ||
| .submodule(&opts.url, opts.path.as_path(), true) | ||
| .with_context(|| { | ||
| format!( | ||
| "Failed to create submodule entry for '{}' from '{}'", | ||
| opts.name, opts.url | ||
| ) | ||
| })?; | ||
|
|
||
| // 2. Configure clone options | ||
| let mut update_opts = git2::SubmoduleUpdateOptions::new(); | ||
| let mut fetch_opts = git2::FetchOptions::new(); | ||
| if opts.shallow { | ||
| fetch_opts.depth(1); | ||
| } | ||
| update_opts.fetch(fetch_opts); | ||
|
|
||
| // 3. Clone the submodule repository | ||
| sub.clone(Some(&mut update_opts)).with_context(|| { | ||
| format!( | ||
| "Failed to clone submodule '{}' from '{}'", | ||
| opts.name, opts.url | ||
| ) | ||
| })?; | ||
|
|
||
| // 4. Add to index and finalize | ||
| sub.add_to_index(true) | ||
| .with_context(|| format!("Failed to add submodule '{}' to index", opts.name))?; | ||
| sub.add_finalize() | ||
| .with_context(|| format!("Failed to finalize submodule '{}'", opts.name))?; | ||
|
|
||
|
Comment on lines
+291
to
+323
|
||
| // 5. Apply optional configuration via git config. | ||
| // git2's submodule() keys the submodule by path; use the path as the config key. | ||
| let path_str = opts.path.to_string_lossy(); | ||
| let mut config = self | ||
| .repo | ||
| .config() | ||
| .with_context(|| "Failed to open git config")?; | ||
|
|
||
| // Set branch if specified | ||
| if let Some(branch) = &opts.branch { | ||
| let branch_key = format!("submodule.{}.branch", path_str); | ||
| config | ||
| .set_str(&branch_key, &branch.to_string()) | ||
| .with_context(|| format!("Failed to set branch for submodule '{}'", opts.name))?; | ||
| } | ||
|
|
||
| // Set ignore rule if specified and not the sentinel Unspecified value | ||
| if let Some(ignore) = &opts.ignore { | ||
| if !matches!(ignore, SerializableIgnore::Unspecified) { | ||
| let ignore_key = format!("submodule.{}.ignore", path_str); | ||
| config | ||
| .set_str(&ignore_key, &ignore.to_string()) | ||
| .with_context(|| { | ||
| format!("Failed to set ignore for submodule '{}'", opts.name) | ||
| })?; | ||
| } | ||
| } | ||
|
|
||
| // Set fetch recurse if specified and not the sentinel Unspecified value | ||
| if let Some(fetch_recurse) = &opts.fetch_recurse { | ||
| if !matches!(fetch_recurse, SerializableFetchRecurse::Unspecified) { | ||
| let fetch_key = format!("submodule.{}.fetchRecurseSubmodules", path_str); | ||
| config | ||
| .set_str(&fetch_key, &fetch_recurse.to_string()) | ||
| .with_context(|| { | ||
| format!("Failed to set fetchRecurse for submodule '{}'", opts.name) | ||
| })?; | ||
| } | ||
| } | ||
|
|
||
| // Set update strategy if specified and not the sentinel Unspecified value | ||
| if let Some(update) = &opts.update { | ||
| if !matches!(update, SerializableUpdate::Unspecified) { | ||
| let update_key = format!("submodule.{}.update", path_str); | ||
| config | ||
|
Comment on lines
+324
to
+368
|
||
| .set_str(&update_key, &update.to_string()) | ||
| .with_context(|| { | ||
| format!("Failed to set update for submodule '{}'", opts.name) | ||
| })?; | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
| fn init_submodule(&mut self, path: &str) -> Result<()> { | ||
| let mut submodule = self | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,11 +3,10 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| // SPDX-License-Identifier: LicenseRef-PlainMIT OR MIT | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| //! Utility functions for working with `Gitoxide` APIs commonly used across the codebase. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| use anyhow::Ok; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use anyhow::Result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use git2::Repository as Git2Repository; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use gix::open::Options; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::path::PathBuf; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::result::Result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Get the current repository using git2, with an optional provided repository. If no repository is provided, it will attempt to discover one in the current directory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub(crate) fn get_current_git2_repository( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -164,20 +163,18 @@ pub(crate) fn name_from_url(url: &str) -> Result<String, anyhow::Error> { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Convert an `OsString` to a `String`, extracting the name from the path | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub(crate) fn name_from_osstring(os_string: std::ffi::OsString) -> Result<String, anyhow::Error> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| osstring_to_string(os_string).and_then(|s| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if s.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if s.contains('\0') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(anyhow::anyhow!("Name cannot contain null bytes")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(s) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let sep = std::path::MAIN_SEPARATOR.to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split(&sep) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .last() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|name| name.to_string()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ok_or_else(|| anyhow::anyhow!("Failed to extract name from OsString")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if s.contains('\0') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Err(anyhow::anyhow!("Name cannot contain null bytes")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if s.trim().is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Err(anyhow::anyhow!("Name cannot be empty or whitespace-only")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let sep = std::path::MAIN_SEPARATOR.to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split(&sep) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .last() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|name| name.to_string()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ok_or_else(|| anyhow::anyhow!("Failed to extract name from OsString")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+169
to
+177
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if s.trim().is_empty() { | |
| return Err(anyhow::anyhow!("Name cannot be empty or whitespace-only")); | |
| } | |
| let sep = std::path::MAIN_SEPARATOR.to_string(); | |
| s.trim() | |
| .split(&sep) | |
| .last() | |
| .map(|name| name.to_string()) | |
| .ok_or_else(|| anyhow::anyhow!("Failed to extract name from OsString")) | |
| let trimmed = s.trim(); | |
| if trimmed.is_empty() { | |
| return Err(anyhow::anyhow!("Name cannot be empty or whitespace-only")); | |
| } | |
| let path = std::path::Path::new(trimmed); | |
| let component = path | |
| .file_name() | |
| .ok_or_else(|| anyhow::anyhow!("Failed to extract name from OsString"))?; | |
| let component_str = component | |
| .to_str() | |
| .ok_or_else(|| anyhow::anyhow!("Failed to convert OsString component to UTF-8"))?; | |
| let name = component_str.trim(); | |
| if name.is_empty() { | |
| return Err(anyhow::anyhow!("Name cannot be empty or whitespace-only")); | |
| } | |
| Ok(name.to_string()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repo.submodule(&opts.url, opts.path.as_path(), true)ignoresopts.name(it only appears in error messages). This makes backend behavior diverge from the CLI fallback (which passes--name) and can lead to the submodule being registered under its path instead of the user-supplied name. Ifopts.nameis intended to be authoritative, adjust the git2 path to ensure.gitmodules/submodule naming matches it (or explicitly document/enforce that name == path for the git2 backend).