diff --git a/README.md b/README.md index 35cad85..30ff5c1 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,15 @@ npm run tauri build Rust commands live in `src-tauri/src/commands`, shared services in `src-tauri/src/services`, and serialized models in `src-tauri/src/models`. The backend uses native Git CLI through `std::process::Command` with argument arrays; shell strings are not used for Git operations. React UI lives in `src/components`, state in `src/store/gitStore.ts`, IPC services in `src/services/gitService.ts`, and shared TypeScript types in `src/types/git.ts`. + +## GitPilot feature coverage + +GitPilot now includes first-pass production workflows for the highest-priority Git operations: + +- Visual conflict resolution parses conflict markers, presents current/incoming/result panes, supports accept-current, accept-incoming, accept-both, manual edits, and stages files after all markers are removed. +- Rebase tooling supports normal rebase, paused-state detection, continue, abort, skip, and an interactive todo editor for pick, reword, edit, squash, fixup, drop, and reorder operations. +- Cherry-pick commands support applying a selected commit and aborting an in-progress cherry-pick so conflicts can route through the same resolver. +- Stash management supports list, push, apply, pop, drop, and rename from the stash panel. +- Productivity primitives are available for blame, fuzzy smart search, and worktree list/create/remove commands. + +Some larger roadmap items, such as provider-backed pull-request review, CI aggregation, enterprise telemetry, and release signing, are intentionally exposed as modular command/service seams for follow-up hardening rather than hard-coded into the UI. diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index b28caf2..273d251 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -173,3 +173,43 @@ pub fn reset_to_commit( }; git_service::git_checked(&repo_path, &["reset", flag, &commit]) } + +#[tauri::command] +pub fn abort_cherry_pick( + repo_path: String, +) -> Result { + git_service::git_checked(&repo_path, &["cherry-pick", "--abort"]) +} + +#[tauri::command] +pub fn blame_file( + repo_path: String, + file_path: String, +) -> Result, GitError> { + let out = git_service::git_text(&repo_path, &["blame", "--line-porcelain", "--", &file_path])?; + let mut res = Vec::new(); + let mut commit = String::new(); + let mut author = String::new(); + let mut time = String::new(); + let mut line_no = 0usize; + for l in out.lines() { + if l.chars().take(40).all(|c| c.is_ascii_hexdigit()) { + let p: Vec<_> = l.split_whitespace().collect(); + commit = p[0].into(); + line_no = p.get(2).and_then(|v| v.parse().ok()).unwrap_or(0); + } else if let Some(v) = l.strip_prefix("author ") { + author = v.into(); + } else if let Some(v) = l.strip_prefix("author-time ") { + time = v.into(); + } else if let Some(v) = l.strip_prefix('\t') { + res.push(crate::models::git::BlameLine { + line_number: line_no, + commit: commit.clone(), + author: author.clone(), + timestamp: time.clone(), + text: v.into(), + }); + } + } + Ok(res) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 5128b9f..f645fb7 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -7,9 +7,11 @@ pub mod merge; pub mod rebase; pub mod remote; pub mod repository; +pub mod search; pub mod settings; pub mod staging; pub mod stash; pub mod status; pub mod tag; pub mod validation; +pub mod worktree; diff --git a/src-tauri/src/commands/rebase.rs b/src-tauri/src/commands/rebase.rs index 3a82463..fe13095 100644 --- a/src-tauri/src/commands/rebase.rs +++ b/src-tauri/src/commands/rebase.rs @@ -18,3 +18,104 @@ pub fn abort_rebase(repo_path: String) -> Result { pub fn skip_rebase(repo_path: String) -> Result { git_service::git_checked(&repo_path, &["rebase", "--skip"]) } + +use crate::models::git::{RebaseState, RebaseTodoItem}; +use std::{fs, path::Path}; + +#[tauri::command] +pub fn get_rebase_state(repo_path: String) -> Result { + let git = Path::new(&repo_path).join(".git"); + let merge = git.join("rebase-merge"); + let apply = git.join("rebase-apply"); + let dir = if merge.exists() { merge } else { apply }; + if !dir.exists() { + return Ok(RebaseState { + in_progress: false, + interactive: false, + current_branch: None, + onto: None, + todo: vec![], + }); + } + let todo_path = dir.join("git-rebase-todo"); + let done_path = dir.join("done"); + let todo_src = fs::read_to_string(&todo_path) + .or_else(|_| fs::read_to_string(&done_path)) + .unwrap_or_default(); + let todo = todo_src + .lines() + .filter_map(|l| { + let line = l.trim(); + if line.is_empty() || line.starts_with('#') { + return None; + } + let mut p = line.splitn(3, ' '); + Some(RebaseTodoItem { + action: p.next()?.into(), + hash: p.next().unwrap_or_default().into(), + message: p.next().unwrap_or_default().into(), + }) + }) + .collect(); + Ok(RebaseState { + in_progress: true, + interactive: todo_path.exists(), + current_branch: fs::read_to_string(dir.join("head-name")) + .ok() + .map(|s| s.trim().trim_start_matches("refs/heads/").into()), + onto: fs::read_to_string(dir.join("onto")) + .ok() + .map(|s| s.trim().into()), + todo, + }) +} + +#[tauri::command] +pub fn start_interactive_rebase( + repo_path: String, + base: String, + todo: Vec, +) -> Result { + let script = todo + .into_iter() + .map(|i| format!("{} {} {}", i.action, i.hash, i.message)) + .collect::>() + .join("\n"); + let dir = std::env::temp_dir(); + let todo_path = dir.join("gitpilot-rebase-todo.txt"); + fs::write(&todo_path, script) + .map_err(|e| GitError::new("TODO_WRITE_FAILED", e.to_string(), ""))?; + let editor_path = if cfg!(windows) { + let path = dir.join("gitpilot-sequence-editor.cmd"); + fs::write( + &path, + format!("@echo off\r\ntype \"{}\" > %1\r\n", todo_path.display()), + ) + .map_err(|e| GitError::new("EDITOR_WRITE_FAILED", e.to_string(), ""))?; + path + } else { + let path = dir.join("gitpilot-sequence-editor.sh"); + fs::write( + &path, + format!("#!/bin/sh\ncat '{}' > \"$1\"\n", todo_path.display()), + ) + .map_err(|e| GitError::new("EDITOR_WRITE_FAILED", e.to_string(), ""))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&path) + .map_err(|e| GitError::new("EDITOR_STAT_FAILED", e.to_string(), ""))? + .permissions(); + perms.set_mode(0o700); + fs::set_permissions(&path, perms) + .map_err(|e| GitError::new("EDITOR_CHMOD_FAILED", e.to_string(), ""))?; + } + path + }; + let editor = editor_path.to_string_lossy().to_string(); + git_service::git_checked_env( + &repo_path, + &["rebase", "-i", &base], + &[(&"GIT_SEQUENCE_EDITOR", &editor)], + ) +} diff --git a/src-tauri/src/commands/search.rs b/src-tauri/src/commands/search.rs new file mode 100644 index 0000000..f25b37c --- /dev/null +++ b/src-tauri/src/commands/search.rs @@ -0,0 +1,71 @@ +use crate::{ + models::git::{GitError, SearchResult}, + services::git_service, +}; + +fn matches(q: &str, text: &str) -> bool { + let q = q.to_lowercase(); + let text = text.to_lowercase(); + let mut it = text.chars(); + q.chars().all(|c| it.any(|x| x == c)) +} +#[tauri::command] +pub fn smart_search( + repo_path: String, + query: String, + limit: u32, +) -> Result, GitError> { + let mut r = Vec::new(); + let lim = limit.to_string(); + let commits = git_service::git_text( + &repo_path, + &[ + "log", + "--all", + "--date=short", + &format!("--max-count={}", lim), + "--pretty=format:%h%x1f%an%x1f%ad%x1f%s", + ], + )?; + for l in commits.lines() { + let p: Vec<_> = l.split('\x1f').collect(); + if p.len() == 4 { + let hay = format!("{} {} {}", p[0], p[1], p[3]); + if matches(&query, &hay) { + r.push(SearchResult { + kind: "commit".into(), + title: p[3].into(), + subtitle: format!("{} · {} · {}", p[0], p[1], p[2]), + target: p[0].into(), + }) + } + } + } + let branches = git_service::git_text( + &repo_path, + &["branch", "--all", "--format=%(refname:short)"], + )?; + for b in branches.lines() { + if matches(&query, b) { + r.push(SearchResult { + kind: "branch".into(), + title: b.into(), + subtitle: "branch".into(), + target: b.into(), + }) + } + } + let files = git_service::git_text(&repo_path, &["ls-files"])?; + for f in files.lines().take(limit as usize * 4) { + if matches(&query, f) { + r.push(SearchResult { + kind: "file".into(), + title: f.into(), + subtitle: "tracked file".into(), + target: f.into(), + }) + } + } + r.truncate(limit as usize); + Ok(r) +} diff --git a/src-tauri/src/commands/stash.rs b/src-tauri/src/commands/stash.rs index 3a4f97b..e47c750 100644 --- a/src-tauri/src/commands/stash.rs +++ b/src-tauri/src/commands/stash.rs @@ -39,3 +39,19 @@ pub fn pop_stash(repo_path: String, stash: String) -> Result Result { git_service::git_checked(&repo_path, &["stash", "drop", &stash]) } + +#[tauri::command] +pub fn rename_stash( + repo_path: String, + stash: String, + message: String, +) -> Result { + let patch = git_service::git_text(&repo_path, &["stash", "show", "-p", &stash])?; + let branch = format!("gitpilot-stash-rename-{}", std::process::id()); + git_service::git_checked(&repo_path, &["stash", "branch", &branch, &stash])?; + let out = git_service::git_checked(&repo_path, &["stash", "push", "-m", message.trim()])?; + let _ = git_service::git_checked(&repo_path, &["checkout", "-"]); + let _ = git_service::git_checked(&repo_path, &["branch", "-D", &branch]); + let _ = patch; + Ok(out) +} diff --git a/src-tauri/src/commands/worktree.rs b/src-tauri/src/commands/worktree.rs new file mode 100644 index 0000000..ca3c737 --- /dev/null +++ b/src-tauri/src/commands/worktree.rs @@ -0,0 +1,64 @@ +use crate::{ + models::git::{GitCommandOutput, GitError, WorktreeInfo}, + services::git_service, +}; + +#[tauri::command] +pub fn list_worktrees(repo_path: String) -> Result, GitError> { + let out = git_service::git_text(&repo_path, &["worktree", "list", "--porcelain"])?; + let mut items = Vec::new(); + let mut cur: Option = None; + for line in out.lines() { + if let Some(path) = line.strip_prefix("worktree ") { + if let Some(item) = cur.take() { + items.push(item); + } + cur = Some(WorktreeInfo { + path: path.into(), + head: String::new(), + branch: None, + bare: false, + detached: false, + }); + } else if let Some(item) = cur.as_mut() { + if let Some(head) = line.strip_prefix("HEAD ") { + item.head = head.into(); + } else if let Some(branch) = line.strip_prefix("branch ") { + item.branch = Some(branch.trim_start_matches("refs/heads/").into()); + } else if line == "bare" { + item.bare = true; + } else if line == "detached" { + item.detached = true; + } + } + } + if let Some(item) = cur { + items.push(item); + } + Ok(items) +} +#[tauri::command] +pub fn create_worktree( + repo_path: String, + path: String, + branch: String, + new_branch: bool, +) -> Result { + if new_branch { + git_service::git_checked(&repo_path, &["worktree", "add", "-b", &branch, &path]) + } else { + git_service::git_checked(&repo_path, &["worktree", "add", &path, &branch]) + } +} +#[tauri::command] +pub fn remove_worktree( + repo_path: String, + path: String, + force: bool, +) -> Result { + if force { + git_service::git_checked(&repo_path, &["worktree", "remove", "--force", &path]) + } else { + git_service::git_checked(&repo_path, &["worktree", "remove", &path]) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7b4595f..15477f6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -42,6 +42,8 @@ pub fn run() { commands::history::cherry_pick_commit, commands::history::revert_commit, commands::history::reset_to_commit, + commands::history::abort_cherry_pick, + commands::history::blame_file, commands::merge::merge_branch, commands::merge::abort_merge, commands::merge::continue_merge, @@ -51,17 +53,24 @@ pub fn run() { commands::rebase::continue_rebase, commands::rebase::abort_rebase, commands::rebase::skip_rebase, + commands::rebase::get_rebase_state, + commands::rebase::start_interactive_rebase, commands::stash::list_stashes, commands::stash::create_stash, commands::stash::apply_stash, commands::stash::pop_stash, commands::stash::drop_stash, + commands::stash::rename_stash, commands::tag::list_tags, commands::tag::create_lightweight_tag, commands::tag::create_annotated_tag, commands::tag::delete_tag, commands::tag::push_tag, commands::validation::run_validation, + commands::worktree::list_worktrees, + commands::worktree::create_worktree, + commands::worktree::remove_worktree, + commands::search::smart_search, commands::ai::ai_complete, commands::ai::explain_diff, commands::ai::generate_commit_message, diff --git a/src-tauri/src/models/git.rs b/src-tauri/src/models/git.rs index 3c45300..dc8e5b3 100644 --- a/src-tauri/src/models/git.rs +++ b/src-tauri/src/models/git.rs @@ -125,3 +125,50 @@ pub struct TagInfo { pub target: String, pub message: String, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RebaseTodoItem { + pub action: String, + pub hash: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RebaseState { + pub in_progress: bool, + pub interactive: bool, + pub current_branch: Option, + pub onto: Option, + pub todo: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlameLine { + pub line_number: usize, + pub commit: String, + pub author: String, + pub timestamp: String, + pub text: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorktreeInfo { + pub path: String, + pub head: String, + pub branch: Option, + pub bare: bool, + pub detached: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SearchResult { + pub kind: String, + pub title: String, + pub subtitle: String, + pub target: String, +} diff --git a/src-tauri/src/services/conflict_parser.rs b/src-tauri/src/services/conflict_parser.rs index 833586c..97a9768 100644 --- a/src-tauri/src/services/conflict_parser.rs +++ b/src-tauri/src/services/conflict_parser.rs @@ -47,3 +47,27 @@ pub fn contains_markers(content: &str) -> bool { .lines() .any(|l| l.starts_with("<<<<<<<") || l.starts_with("=======") || l.starts_with(">>>>>>>")) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_multiple_conflict_blocks() { + let parsed = parse( + "file.txt".into(), + "a\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> feature\nb\n<<<<<<< HEAD\n1\n=======\n2\n>>>>>>> feature\n".into(), + ); + assert!(parsed.has_markers); + assert_eq!(parsed.blocks.len(), 2); + assert_eq!(parsed.blocks[0].current, "ours"); + assert_eq!(parsed.blocks[0].incoming, "theirs"); + assert_eq!(parsed.blocks[1].start_line, 8); + } + + #[test] + fn detects_any_marker_line() { + assert!(contains_markers("x\n=======\ny")); + assert!(!contains_markers("plain text")); + } +} diff --git a/src-tauri/src/services/git_service.rs b/src-tauri/src/services/git_service.rs index 51b887b..f401126 100644 --- a/src-tauri/src/services/git_service.rs +++ b/src-tauri/src/services/git_service.rs @@ -35,3 +35,39 @@ pub fn git_checked(repo: &str, args: &[&str]) -> Result Result { Ok(git_checked(repo, args)?.stdout) } + +pub fn git_checked_env( + repo: &str, + args: &[&str], + envs: &[(&&str, &String)], +) -> Result { + let mut cmd = Command::new("git"); + cmd.args(args).current_dir(repo); + for (k, v) in envs { + cmd.env(**k, v); + } + let out = cmd + .output() + .map_err(|e| GitError::new("GIT_SPAWN_FAILED", format!("Failed to run git: {e}"), ""))?; + let stdout = String::from_utf8_lossy(&out.stdout).to_string(); + let stderr = String::from_utf8_lossy(&out.stderr).to_string(); + let r = GitCommandOutput { + stdout, + stderr: stderr.clone(), + success: out.status.success(), + command: format!("git {}", args.join(" ")), + }; + if r.success { + Ok(r) + } else { + Err(GitError::new( + "GIT_COMMAND_FAILED", + if stderr.trim().is_empty() { + r.stdout.clone() + } else { + stderr.clone() + }, + stderr, + )) + } +} diff --git a/src/components/rebase/RebasePanel.tsx b/src/components/rebase/RebasePanel.tsx index 11c28dd..40539eb 100644 --- a/src/components/rebase/RebasePanel.tsx +++ b/src/components/rebase/RebasePanel.tsx @@ -1 +1,3 @@ -import { useGitStore } from '../../store/gitStore';import { gitService } from '../../services/gitService';export function RebasePanel(){const s=useGitStore();const repo=s.repo?.path;return

Rebase

{s.branches.map(b=>)}
} +import { useEffect,useState } from 'react';import { useGitStore } from '../../store/gitStore';import { gitService } from '../../services/gitService';import type { RebaseState,RebaseTodoItem } from '../../types/git'; +const actions=['pick','reword','edit','squash','fixup','drop']; +export function RebasePanel(){const s=useGitStore();const repo=s.repo?.path;const [state,setState]=useState();const [base,setBase]=useState('HEAD~5');const [todo,setTodo]=useState([]);useEffect(()=>{if(repo)void gitService.getRebaseState(repo).then(setState)},[repo,s.status.mergeState.isRebasing]);const move=(i:number,d:number)=>{const n=[...todo];const [x]=n.splice(i,1);n.splice(i+d,0,x);setTodo(n)};const load=()=>setTodo(s.history.slice(0,20).map(c=>({action:'pick',hash:c.hash,message:c.message})).reverse());return

Rebase

{state?.inProgress?
Paused rebase {state.interactive?'(interactive)':''} onto {state.onto||'unknown'}
:null}
{s.branches.filter(b=>!b.current).map(b=>)}
setBase(e.target.value)} placeholder="Base e.g. main or HEAD~5"/>
{todo.map((t,i)=>
e.dataTransfer.setData('text/plain',String(i))} onDrop={e=>{const from=Number(e.dataTransfer.getData('text/plain'));const n=[...todo];const [x]=n.splice(from,1);n.splice(i,0,x);setTodo(n)}} onDragOver={e=>e.preventDefault()} className="mb-1 flex items-center gap-2 rounded bg-slate-900 p-1 text-xs">{t.hash.slice(0,8)}{t.message}
)}
} diff --git a/src/components/stash/StashPanel.tsx b/src/components/stash/StashPanel.tsx index 24a9dfc..c85a652 100644 --- a/src/components/stash/StashPanel.tsx +++ b/src/components/stash/StashPanel.tsx @@ -1 +1,2 @@ -import { useGitStore } from '../../store/gitStore';import { gitService } from '../../services/gitService';export function StashPanel(){const s=useGitStore();const repo=s.repo?.path;return
{s.stashes.map(st=>
{st.name}
)}
} +import { useGitStore } from '../../store/gitStore';import { gitService } from '../../services/gitService'; +export function StashPanel(){const s=useGitStore();const repo=s.repo?.path;return

Stashes

{s.stashes.map(st=>
{st.name}
{st.message||st.branch}
)}
} diff --git a/src/services/gitService.ts b/src/services/gitService.ts index 69f1720..4dd00a1 100644 --- a/src/services/gitService.ts +++ b/src/services/gitService.ts @@ -1,6 +1,6 @@ import { invoke } from '@tauri-apps/api/core'; import { demoHistory, demoRepo, demoStatus } from '../demo/currentRepoDemo'; -import type {AiResponse,BranchInfo,CommitFile,CommitInfo,DiffResult,GitCommandOutput,GitStatus,ParsedConflictFile,RemoteInfo,RepositoryInfo,Settings,StashInfo,TagInfo,HistoryFilters} from '../types/git'; +import type {AiResponse,BranchInfo,CommitFile,CommitInfo,DiffResult,GitCommandOutput,GitStatus,ParsedConflictFile,RemoteInfo,RepositoryInfo,Settings,StashInfo,TagInfo,HistoryFilters,RebaseState,RebaseTodoItem,BlameLine,WorktreeInfo,SearchResult} from '../types/git'; const isTauriRuntime=()=>typeof window!=='undefined'&&'__TAURI_INTERNALS__' in window; const demoSettings={theme:'dark',gitPath:'git',defaultTargetBranch:'main',recentRepositories:[demoRepo.path],aiProvider:'ollama',aiApiKey:'',aiModel:'',validationCommands:[],shortcuts:[]}; @@ -30,10 +30,10 @@ export const gitService={ getDiff:(repoPath:string,filePath:string,cached:boolean)=>call('get_diff',{repoPath,filePath,cached}), getCommitFileDiff:(repoPath:string,commit:string,filePath:string)=>call('get_commit_file_diff',{repoPath,commit,filePath}), commit:(repoPath:string,message:string,amend:boolean)=>call('commit',{repoPath,message,amend}), stagedDiff:(repoPath:string)=>call('staged_diff',{repoPath}), listBranches:(repoPath:string)=>call('list_branches',{repoPath}), createBranch:(repoPath:string,name:string,checkout:boolean)=>call('create_branch',{repoPath,name,checkout}), checkoutBranch:(repoPath:string,name:string)=>call('checkout_branch',{repoPath,name}), renameBranch:(repoPath:string,oldName:string,newName:string)=>call('rename_branch',{repoPath,oldName,newName}), deleteBranch:(repoPath:string,name:string,force:boolean)=>call('delete_branch',{repoPath,name,force}), compareBranch:(repoPath:string,branch:string)=>call('compare_branch',{repoPath,branch}), listRemotes:(repoPath:string)=>call('list_remotes',{repoPath}), fetch:(repoPath:string,remote='origin')=>call('fetch',{repoPath,remote}), pull:(repoPath:string)=>call('pull',{repoPath}), push:(repoPath:string)=>call('push',{repoPath}), pushNewBranch:(repoPath:string,remote:string,branch:string)=>call('push_new_branch',{repoPath,remote,branch}), - getHistory:(repoPath:string,limit=500,filters:HistoryFilters={})=>call('get_history',{repoPath,limit,branch:filters.branch,author:filters.author,since:filters.since,until:filters.until,keyword:filters.keyword,filePath:filters.filePath}), getCommitFiles:(repoPath:string,commit:string)=>call('get_commit_files',{repoPath,commit}), compareCommits:(repoPath:string,from:string,to:string)=>call('compare_commits',{repoPath,from,to}), checkoutCommit:(repoPath:string,commit:string)=>call('checkout_commit',{repoPath,commit}), createBranchFromCommit:(repoPath:string,name:string,commit:string,checkout:boolean)=>call('create_branch_from_commit',{repoPath,name,commit,checkout}), createTagFromCommit:(repoPath:string,name:string,commit:string)=>call('create_tag_from_commit',{repoPath,name,commit}), cherryPickCommit:(repoPath:string,commit:string)=>call('cherry_pick_commit',{repoPath,commit}), revertCommit:(repoPath:string,commit:string)=>call('revert_commit',{repoPath,commit}), resetToCommit:(repoPath:string,commit:string,mode:'soft'|'mixed'|'hard')=>call('reset_to_commit',{repoPath,commit,mode}), + getHistory:(repoPath:string,limit=500,filters:HistoryFilters={})=>call('get_history',{repoPath,limit,branch:filters.branch,author:filters.author,since:filters.since,until:filters.until,keyword:filters.keyword,filePath:filters.filePath}), getCommitFiles:(repoPath:string,commit:string)=>call('get_commit_files',{repoPath,commit}), compareCommits:(repoPath:string,from:string,to:string)=>call('compare_commits',{repoPath,from,to}), checkoutCommit:(repoPath:string,commit:string)=>call('checkout_commit',{repoPath,commit}), createBranchFromCommit:(repoPath:string,name:string,commit:string,checkout:boolean)=>call('create_branch_from_commit',{repoPath,name,commit,checkout}), createTagFromCommit:(repoPath:string,name:string,commit:string)=>call('create_tag_from_commit',{repoPath,name,commit}), cherryPickCommit:(repoPath:string,commit:string)=>call('cherry_pick_commit',{repoPath,commit}), abortCherryPick:(repoPath:string)=>call('abort_cherry_pick',{repoPath}), blameFile:(repoPath:string,filePath:string)=>call('blame_file',{repoPath,filePath}), revertCommit:(repoPath:string,commit:string)=>call('revert_commit',{repoPath,commit}), resetToCommit:(repoPath:string,commit:string,mode:'soft'|'mixed'|'hard')=>call('reset_to_commit',{repoPath,commit,mode}), mergeBranch:(repoPath:string,branch:string)=>call('merge_branch',{repoPath,branch}), abortMerge:(repoPath:string)=>call('abort_merge',{repoPath}), continueMerge:(repoPath:string)=>call('continue_merge',{repoPath}), parseConflictFile:(repoPath:string,filePath:string)=>call('parse_conflict_file',{repoPath,filePath}), saveResolvedFile:(repoPath:string,filePath:string,content:string)=>call('save_resolved_file',{repoPath,filePath,content}), - startRebase:(repoPath:string,onto:string)=>call('start_rebase',{repoPath,onto}), continueRebase:(repoPath:string)=>call('continue_rebase',{repoPath}), abortRebase:(repoPath:string)=>call('abort_rebase',{repoPath}), skipRebase:(repoPath:string)=>call('skip_rebase',{repoPath}), - listStashes:(repoPath:string)=>call('list_stashes',{repoPath}), createStash:(repoPath:string,message:string)=>call('create_stash',{repoPath,message}), applyStash:(repoPath:string,stash:string)=>call('apply_stash',{repoPath,stash}), popStash:(repoPath:string,stash:string)=>call('pop_stash',{repoPath,stash}), dropStash:(repoPath:string,stash:string)=>call('drop_stash',{repoPath,stash}), + startRebase:(repoPath:string,onto:string)=>call('start_rebase',{repoPath,onto}), startInteractiveRebase:(repoPath:string,base:string,todo:RebaseTodoItem[])=>call('start_interactive_rebase',{repoPath,base,todo}), getRebaseState:(repoPath:string)=>call('get_rebase_state',{repoPath}), continueRebase:(repoPath:string)=>call('continue_rebase',{repoPath}), abortRebase:(repoPath:string)=>call('abort_rebase',{repoPath}), skipRebase:(repoPath:string)=>call('skip_rebase',{repoPath}), + listStashes:(repoPath:string)=>call('list_stashes',{repoPath}), createStash:(repoPath:string,message:string)=>call('create_stash',{repoPath,message}), applyStash:(repoPath:string,stash:string)=>call('apply_stash',{repoPath,stash}), popStash:(repoPath:string,stash:string)=>call('pop_stash',{repoPath,stash}), dropStash:(repoPath:string,stash:string)=>call('drop_stash',{repoPath,stash}), renameStash:(repoPath:string,stash:string,message:string)=>call('rename_stash',{repoPath,stash,message}), listTags:(repoPath:string)=>call('list_tags',{repoPath}), createLightweightTag:(repoPath:string,name:string)=>call('create_lightweight_tag',{repoPath,name}), createAnnotatedTag:(repoPath:string,name:string,message:string)=>call('create_annotated_tag',{repoPath,name,message}), deleteTag:(repoPath:string,name:string)=>call('delete_tag',{repoPath,name}), pushTag:(repoPath:string,remote:string,name:string)=>call('push_tag',{repoPath,remote,name}), - runValidation:(repoPath:string)=>call('run_validation',{repoPath}), explainDiff:(repoPath:string,diff:string,provider:string,model:string)=>call('explain_diff',{repoPath,diff,provider,model}), generateCommitMessage:(repoPath:string,provider:string,model:string)=>call('generate_commit_message',{repoPath,provider,model}), suggestBranchName:(description:string,provider:string,model:string)=>call('suggest_branch_name',{description,provider,model}), resolveConflictBlock:(current:string,incoming:string,provider:string,model:string)=>call('resolve_conflict_block',{current,incoming,provider,model}), getSettings:()=>call('get_settings'), saveSettings:(settings:Settings)=>call('save_settings',{settings}) + runValidation:(repoPath:string)=>call('run_validation',{repoPath}), listWorktrees:(repoPath:string)=>call('list_worktrees',{repoPath}), createWorktree:(repoPath:string,path:string,branch:string,newBranch:boolean)=>call('create_worktree',{repoPath,path,branch,newBranch}), removeWorktree:(repoPath:string,path:string,force=false)=>call('remove_worktree',{repoPath,path,force}), smartSearch:(repoPath:string,query:string,limit=25)=>call('smart_search',{repoPath,query,limit}), explainDiff:(repoPath:string,diff:string,provider:string,model:string)=>call('explain_diff',{repoPath,diff,provider,model}), generateCommitMessage:(repoPath:string,provider:string,model:string)=>call('generate_commit_message',{repoPath,provider,model}), suggestBranchName:(description:string,provider:string,model:string)=>call('suggest_branch_name',{description,provider,model}), resolveConflictBlock:(current:string,incoming:string,provider:string,model:string)=>call('resolve_conflict_block',{current,incoming,provider,model}), getSettings:()=>call('get_settings'), saveSettings:(settings:Settings)=>call('save_settings',{settings}) }; diff --git a/src/types/git.ts b/src/types/git.ts index 20d3883..cd24954 100644 --- a/src/types/git.ts +++ b/src/types/git.ts @@ -16,3 +16,9 @@ export type ConflictBlock={id:number;startLine:number;separatorLine:number;endLi export type ParsedConflictFile={path:string;content:string;blocks:ConflictBlock[];hasMarkers:boolean}; export type Settings={theme:string;gitPath:string;defaultTargetBranch:string;recentRepositories:string[];aiProvider:string;aiApiKey:string;aiModel:string;validationCommands:string[];shortcuts:string[]}; export type AiResponse={text:string;requiresReview:boolean}; + +export type RebaseTodoItem={action:string;hash:string;message:string}; +export type RebaseState={inProgress:boolean;interactive:boolean;currentBranch?:string|null;onto?:string|null;todo:RebaseTodoItem[]}; +export type BlameLine={lineNumber:number;commit:string;author:string;timestamp:string;text:string}; +export type WorktreeInfo={path:string;head:string;branch?:string|null;bare:boolean;detached:boolean}; +export type SearchResult={kind:string;title:string;subtitle:string;target:string};