Skip to content
Merged
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
45 changes: 41 additions & 4 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4779,7 +4779,7 @@ fn create_session_inner(
name = project_basename(&repo);
}
let base = sanitize_worktree_name(&name);
let (_safe_name, path) = create_unique_worktree(&repo, &base)?;
let (_safe_name, path) = create_unique_project_worktree(&repo, &base)?;
path
} else {
cwd_path.unwrap_or(selected_path)
Expand Down Expand Up @@ -6747,7 +6747,17 @@ pub fn update_project_settings(
repo_path: String,
settings: ProjectSettings,
) -> AppResult<ProjectSettingsRecord> {
project_settings::update(&PathBuf::from(repo_path), settings)
let repo_path = PathBuf::from(repo_path);
if let Some(branch) = settings
.worktrees
.base_branch
.as_deref()
.map(str::trim)
.filter(|branch| !branch.is_empty())
{
worktree::validate_worktree_base_branch(&repo_path, branch)?;
}
project_settings::update(&repo_path, settings)
}

fn validate_new_project_name(name: &str, ignore_safe_name: bool) -> AppResult<&str> {
Expand Down Expand Up @@ -7481,7 +7491,7 @@ pub fn prepare_chat_session_worktree(
return Ok(enrich_session(session));
}
let base = new_chat_worktree_base_name(&session.repo_path);
let (_safe_name, path) = create_unique_worktree(&session.repo_path, &base)?;
let (_safe_name, path) = create_unique_project_worktree(&session.repo_path, &base)?;
let updated = state.sessions.update_worktree_path(&session.id, path)?;
persist(&state);
Ok(enrich_session(updated))
Expand Down Expand Up @@ -7844,6 +7854,12 @@ pub fn list_project_worktrees(repo_path: String) -> AppResult<Vec<worktree::Proj
crate::worktree::list_worktree_infos(&path)
}

#[tauri::command]
pub fn list_project_branches(repo_path: String) -> AppResult<Vec<worktree::ProjectBranchInfo>> {
let path = PathBuf::from(repo_path);
crate::worktree::list_branch_infos(&path)
}

#[tauri::command]
pub async fn remove_worktree(
state: State<'_, AppState>,
Expand Down Expand Up @@ -10745,17 +10761,38 @@ pub async fn generate_pr_commit_message(
pull_requests::generate_pr_commit_message(&PathBuf::from(repo_path), number, method, ai, prompt)
}

#[cfg(test)]
pub(crate) fn create_unique_worktree(
repo: &std::path::Path,
base: &str,
) -> AppResult<(String, PathBuf)> {
create_unique_worktree_from_base_branch(repo, base, None)
}

pub(crate) fn create_unique_project_worktree(
repo: &std::path::Path,
base: &str,
) -> AppResult<(String, PathBuf)> {
let settings = project_settings::get(repo)?;
create_unique_worktree_from_base_branch(
repo,
base,
settings.settings.worktrees.base_branch.as_deref(),
)
}

fn create_unique_worktree_from_base_branch(
repo: &std::path::Path,
base: &str,
base_branch: Option<&str>,
) -> AppResult<(String, PathBuf)> {
let root = worktree::worktree_root(repo);
let mut candidate = base.to_string();
let mut n = 2;
loop {
let target = root.join(&candidate);
if !target.exists() {
match worktree::create_worktree(repo, &candidate) {
match worktree::create_worktree_from_base_branch(repo, &candidate, base_branch) {
Ok(path) => return Ok((candidate, path)),
Err(AppError::InvalidPath(_)) => {}
// libgit2 auto-creates a branch named after the worktree when
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/ipc/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ use tauri::{AppHandle, Emitter, Runtime};
use uuid::Uuid;

use crate::commands::{
create_unique_worktree, sanitize_worktree_name, session_removal_cascade, terminate_session_pty,
create_unique_project_worktree, sanitize_worktree_name, session_removal_cascade,
terminate_session_pty,
};
use crate::ipc::workspaces::{ListWorkspacesRequestPayload, LIST_WORKSPACES_REQUEST_EVENT};
use crate::persistence;
Expand Down Expand Up @@ -737,7 +738,7 @@ fn handle_new_session<R: Runtime>(
};
let worktree_path = if isolated {
let base = sanitize_worktree_name(&name);
match create_unique_worktree(&repo, &base) {
match create_unique_project_worktree(&repo, &base) {
Ok((_safe, path)) => path,
Err(err) => {
return Response::Error {
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ pub fn run() {
commands::prepare_chat_session_worktree,
commands::git_worktrees,
commands::list_project_worktrees,
commands::list_project_branches,
commands::remove_worktree,
commands::restore_removed_worktree,
commands::discard_removed_worktree,
Expand Down
46 changes: 46 additions & 0 deletions src-tauri/src/project_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{git_ops, persistence};
const PROJECT_SETTINGS_FILE: &str = "project_settings.json";
const PROJECT_SETTINGS_TMP_FILE: &str = "project_settings.json.tmp";
pub const PR_GENERATION_PROMPT_MAX_CHARS: usize = 2_000;
pub const WORKTREE_BASE_BRANCH_MAX_CHARS: usize = 255;
pub const STANDARD_PR_GENERATION_PROMPT: &str = "Use a standard GitHub-style pull request merge message.
- First line: Conventional Commit subject when the type is clear, e.g. feat(scope): concise summary. Keep it imperative/present tense and <=72 chars.
- Body: 1-2 concise paragraphs explaining why the change matters, user-visible impact, and key implementation notes when useful.
Expand All @@ -21,13 +22,16 @@ pub struct ProjectSettings {
pub remember_after_close: bool,
#[serde(default)]
pub pull_requests: ProjectPullRequestSettings,
#[serde(default)]
pub worktrees: ProjectWorktreeSettings,
}

impl Default for ProjectSettings {
fn default() -> Self {
Self {
remember_after_close: true,
pull_requests: ProjectPullRequestSettings::default(),
worktrees: ProjectWorktreeSettings::default(),
}
}
}
Expand All @@ -46,6 +50,12 @@ impl Default for ProjectPullRequestSettings {
}
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectWorktreeSettings {
#[serde(default)]
pub base_branch: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProjectSettingsRecord {
pub key: String,
Expand Down Expand Up @@ -140,6 +150,19 @@ fn normalize_settings(mut settings: ProjectSettings) -> ProjectSettings {
)
}
});
settings.worktrees.base_branch = settings.worktrees.base_branch.and_then(|branch| {
let trimmed = branch.trim();
if trimmed.is_empty() {
None
} else {
Some(
trimmed
.chars()
.take(WORKTREE_BASE_BRANCH_MAX_CHARS)
.collect(),
)
}
});
settings
}

Expand Down Expand Up @@ -171,6 +194,9 @@ mod tests {
pull_requests: ProjectPullRequestSettings {
generation_prompt: Some("Write concise Korean PR messages.".to_string()),
},
worktrees: ProjectWorktreeSettings {
base_branch: Some("develop".to_string()),
},
};

let saved = update(&repo, settings).unwrap();
Expand All @@ -182,6 +208,10 @@ mod tests {
loaded.settings.pull_requests.generation_prompt.as_deref(),
Some("Write concise Korean PR messages.")
);
assert_eq!(
loaded.settings.worktrees.base_branch.as_deref(),
Some("develop")
);
});
}

Expand All @@ -201,6 +231,17 @@ mod tests {
});
}

#[test]
fn settings_without_worktree_fields_use_automatic_base_branch() {
let settings: ProjectSettings = serde_json::from_value(serde_json::json!({
"remember_after_close": true,
"pull_requests": { "generation_prompt": null }
}))
.unwrap();

assert_eq!(settings.worktrees, ProjectWorktreeSettings::default());
}

#[test]
fn blank_prompt_normalizes_to_none() {
with_data_dir(|_| {
Expand All @@ -213,6 +254,9 @@ mod tests {
pull_requests: ProjectPullRequestSettings {
generation_prompt: Some(" ".to_string()),
},
worktrees: ProjectWorktreeSettings {
base_branch: Some(" ".to_string()),
},
},
)
.unwrap();
Expand All @@ -221,6 +265,7 @@ mod tests {
get(&repo).unwrap().settings.pull_requests.generation_prompt,
None
);
assert_eq!(get(&repo).unwrap().settings.worktrees.base_branch, None);
});
}

Expand All @@ -236,6 +281,7 @@ mod tests {
ProjectSettings {
remember_after_close: false,
pull_requests: ProjectPullRequestSettings::default(),
worktrees: ProjectWorktreeSettings::default(),
},
)
.unwrap();
Expand Down
Loading
Loading