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
3 changes: 3 additions & 0 deletions src/ps/private/config/config_dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::super::utils;
use super::branch::BranchConfigDto;
use super::fetch::FetchConfigDto;
use super::integrate::IntegrateConfigDto;
use super::isolate::IsolateConfigDto;
use super::list::ListConfigDto;
use super::pull::PullConfigDto;
use super::request_review::RequestReviewConfigDto;
Expand All @@ -15,6 +16,7 @@ pub struct ConfigDto {
pub fetch: Option<FetchConfigDto>,
pub list: Option<ListConfigDto>,
pub branch: Option<BranchConfigDto>,
pub isolate: Option<IsolateConfigDto>,
}

impl utils::Mergable for ConfigDto {
Expand All @@ -26,6 +28,7 @@ impl utils::Mergable for ConfigDto {
fetch: utils::merge_option(&self.fetch, &b.fetch),
list: utils::merge_option(&self.list, &b.list),
branch: utils::merge_option(&self.branch, &b.branch),
isolate: utils::merge_option(&self.isolate, &b.isolate),
}
}
}
17 changes: 16 additions & 1 deletion src/ps/private/config/get_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use super::config_dto::ConfigDto;
use super::fetch::FetchConfigDto;
use super::integrate::IntegrateConfigDto;
use super::list::{ColorWithAlternate, ListConfigDto};
use super::isolate::IsolateConfigDto; // NEW
use super::ps_config::{
PsConfig, PsFetchConfig, PsIntegrateConfig, PsListConfig, PsPullConfig, PsRequestReviewConfig,
PsConfig, PsFetchConfig, PsIntegrateConfig, PsIsolateConfig, PsListConfig, PsPullConfig, PsRequestReviewConfig, // NEW: PsIsolateConfig
};
use super::pull::PullConfigDto;
use super::read_config_or_default::*;
Expand Down Expand Up @@ -68,6 +69,7 @@ fn apply_config_defaults(config_dto: &ConfigDto) -> PsConfig {
let default_integrate_config = apply_integrate_config_defaults(&IntegrateConfigDto::default());
let default_fetch_config = apply_fetch_config_defaults(&FetchConfigDto::default());
let default_list_config = apply_list_config_defaults(&ListConfigDto::default());
let default_isolate_config = apply_isolate_config_defaults(&IsolateConfigDto::default()); // NEW
PsConfig {
request_review: config_dto
.request_review
Expand All @@ -94,6 +96,11 @@ fn apply_config_defaults(config_dto: &ConfigDto) -> PsConfig {
.as_ref()
.map(apply_list_config_defaults)
.unwrap_or(default_list_config),
isolate: config_dto // NEW
.isolate
.as_ref()
.map(apply_isolate_config_defaults)
.unwrap_or(default_isolate_config),
}
}

Expand Down Expand Up @@ -179,3 +186,11 @@ fn apply_list_config_defaults(list_config_dto: &ListConfigDto) -> PsListConfig {
}),
}
}

// NEW function
fn apply_isolate_config_defaults(isolate_config_dto: &IsolateConfigDto) -> PsIsolateConfig {
PsIsolateConfig {
exclude_submodules: isolate_config_dto.exclude_submodules.unwrap_or(true), // Default to true (exclude submodules)
include_untracked: isolate_config_dto.include_untracked.unwrap_or(false), // Default to false (don't include untracked)
}
}
19 changes: 19 additions & 0 deletions src/ps/private/config/isolate/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::ps::private::utils;
use serde::Deserialize;
use std::option::Option;

#[derive(Debug, Deserialize, Clone, Default)]
pub struct IsolateConfigDto {
pub exclude_submodules: Option<bool>,
pub include_untracked: Option<bool>,
}

impl utils::Mergable for IsolateConfigDto {
/// Merge the provided b with self overriding with any present values
fn merge(&self, b: &Self) -> Self {
IsolateConfigDto {
exclude_submodules: b.exclude_submodules.or(self.exclude_submodules),
include_untracked: b.include_untracked.or(self.include_untracked),
}
}
}
1 change: 1 addition & 0 deletions src/ps/private/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod integrate;
pub mod list;
pub mod pull;
pub mod request_review;
pub mod isolate;

mod config_dto;
mod get_config;
Expand Down
7 changes: 7 additions & 0 deletions src/ps/private/config/ps_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ pub struct PsConfig {
pub integrate: PsIntegrateConfig,
pub fetch: PsFetchConfig,
pub list: PsListConfig,
pub isolate: PsIsolateConfig,
}

#[derive(Debug)]
pub struct PsIsolateConfig {
pub exclude_submodules: bool,
pub include_untracked: bool,
}

#[derive(Debug)]
Expand Down
9 changes: 7 additions & 2 deletions src/ps/private/git/uncommited_changes_exist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ impl std::error::Error for UncommittedChangesError {
}
}

pub fn uncommitted_changes_exist(repo: &git2::Repository) -> Result<bool, UncommittedChangesError> {
pub fn uncommitted_changes_exist(
repo: &git2::Repository,
exclude_submodules: bool,
include_untracked: bool,
) -> Result<bool, UncommittedChangesError> {
let mut status_options = git2::StatusOptions::default();
status_options.show(git2::StatusShow::Workdir);
status_options.include_untracked(true);
status_options.include_untracked(include_untracked);
status_options.exclude_submodules(exclude_submodules);
let statuses = repo
.statuses(Some(&mut status_options))
.map_err(UncommittedChangesError::StatusesFailed)?;
Expand Down
18 changes: 14 additions & 4 deletions src/ps/public/isolate.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::super::super::ps;
use super::super::private::cherry_picking;
use super::super::private::config;
use super::super::private::git;
use super::super::private::hooks;
use super::super::private::paths;
Expand All @@ -11,6 +12,7 @@ use std::result::Result;
pub enum IsolateError {
OpenGitRepositoryFailed(Box<dyn std::error::Error>),
OpenGitConfigFailed(Box<dyn std::error::Error>),
GetConfigFailed(Box<dyn std::error::Error>),
UncommittedChangesExistFailure(Box<dyn std::error::Error>),
UncommittedChangesExist,
GetPatchStackFailed(Box<dyn std::error::Error>),
Expand Down Expand Up @@ -60,6 +62,7 @@ impl std::fmt::Display for IsolateError {
match self {
Self::OpenGitRepositoryFailed(e) => write!(f, "failed to open git repository, {}", e),
Self::OpenGitConfigFailed(e) => write!(f, "failed to open git config, {}", e),
Self::GetConfigFailed(e) => write!(f, "failed to get config, {}", e),
Self::UncommittedChangesExistFailure(e) => {
write!(f, "checking for uncommitted changes failed, {}", e)
}
Expand Down Expand Up @@ -113,6 +116,7 @@ impl std::error::Error for IsolateError {
match self {
Self::OpenGitRepositoryFailed(e) => Some(e.as_ref()),
Self::OpenGitConfigFailed(e) => Some(e.as_ref()),
Self::GetConfigFailed(e) => Some(e.as_ref()),
Self::UncommittedChangesExistFailure(e) => Some(e.as_ref()),
Self::UncommittedChangesExist => None,
Self::GetPatchStackFailed(e) => Some(e.as_ref()),
Expand Down Expand Up @@ -154,12 +158,18 @@ pub fn isolate(
let repo = ps::private::git::create_cwd_repo()
.map_err(|e| IsolateError::OpenGitRepositoryFailed(e.into()))?;

let repo_root_path = paths::repo_root_path(&repo)
.map_err(|e| IsolateError::GetRepoRootPathFailed(e.into()))?;
let repo_root_str = repo_root_path.to_str().ok_or(IsolateError::PathNotUtf8)?;
let repo_gitdir_path = repo.path();
let repo_gitdir_str = repo_gitdir_path.to_str().ok_or(IsolateError::PathNotUtf8)?;
let config =

let config = config::get_config(repo_root_str, repo_gitdir_str)
.map_err(|e| IsolateError::GetConfigFailed(e.into()))?;
let git_config =
git2::Config::open_default().map_err(|e| IsolateError::OpenGitConfigFailed(e.into()))?;

if git::uncommitted_changes_exist(&repo)
if git::uncommitted_changes_exist(&repo, config.isolate.exclude_submodules, config.isolate.include_untracked)
.map_err(|e| IsolateError::UncommittedChangesExistFailure(e.into()))?
{
return Err(IsolateError::UncommittedChangesExist);
Expand Down Expand Up @@ -192,7 +202,7 @@ pub fn isolate(

cherry_picking::cherry_pick(
&repo,
&config,
&git_config,
cherry_pick_range.root_oid,
cherry_pick_range.leaf_oid,
branch_ref_name,
Expand Down Expand Up @@ -310,4 +320,4 @@ pub fn isolate(
Ok(())
}
}
}
}
101 changes: 101 additions & 0 deletions test_isolate_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/bash
set -e

# --- Configuration ---
GPS_BINARY="${PWD}/target/release/gps"
TEST_ROOT_DIR="/tmp/git-ps-isolate-config-test"
REPO_DIR="${TEST_ROOT_DIR}/repo"
ORIGIN_REMOTE_DIR="${TEST_ROOT_DIR}/origin.git"

USER_NAME="Test User"
USER_EMAIL="test@example.com"

# --- Helper Functions ---
cleanup() {
echo "Cleaning up test directory..."
rm -rf "${TEST_ROOT_DIR}"
}

# Sets up a repository with one patch, ready for 'gps isolate 0'
setup_repo() {
cleanup
echo "Setting up repositories..."
mkdir -p "${ORIGIN_REMOTE_DIR}"
git -C "${ORIGIN_REMOTE_DIR}" init --bare

mkdir -p "${REPO_DIR}"
cd "${REPO_DIR}"

git init -b main
git config user.email "${USER_EMAIL}"
git config user.name "${USER_NAME}"

git remote add origin "${ORIGIN_REMOTE_DIR}"

# Create a base commit and push it to establish the remote tracking branch
git commit --allow-empty -m "Initial commit"
git push origin main
git config branch.main.remote origin
git config branch.main.merge refs/heads/main

# Create a patch
git commit --allow-empty -m "feat: A new feature"
}

# --- Test Functions ---

test_defaults() {
echo
echo "--- Testing Default Behavior (no config) ---"
setup_repo

echo "Creating an untracked file..."
echo "untracked content" > untracked.txt

echo "Running 'gps isolate 0'..."
# The default is include_untracked = false, so this should succeed
if "${GPS_BINARY}" isolate 0; then
echo "SUCCESS: 'gps isolate' succeeded with an untracked file, as expected by default."
else
echo "FAILURE: 'gps isolate' failed unexpectedly."
exit 1
fi
cleanup
}

test_configured() {
echo
echo "--- Testing Configured Behavior (with config.toml) ---"
setup_repo

echo "Creating .git-ps/config.toml to include untracked files..."
mkdir -p .git-ps
cat > .git-ps/config.toml << EOL
[isolate]
include_untracked = true
EOL

echo "Creating an untracked file..."
echo "untracked content" > untracked.txt

echo "Running 'gps isolate 0'..."
# We expect this to fail because include_untracked = true
# We redirect stderr to stdout to grep for the error message
if ! "${GPS_BINARY}" isolate 0 2>&1 | grep -q "yours is dirty"; then
echo "FAILURE: 'gps isolate' did not fail with the expected 'dirty' message."
exit 1
else
echo "SUCCESS: 'gps isolate' failed as expected when configured to include untracked files."
fi
cleanup
}

# --- Main Execution ---
echo "Building 'gps' executable..."
cargo build --release

test_defaults
test_configured

echo
echo "All isolate config tests passed successfully."
Loading