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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
uses: actions/setup-node@v5
with:
node-version: 24
cache: npm

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
Expand Down
92 changes: 92 additions & 0 deletions docs/production-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# GitPilot Production Audit and GitKraken-Parity Roadmap

## Executive summary
GitPilot has a promising Tauri/Rust command boundary, a React/Zustand frontend, and broad Git porcelain coverage, but it is not yet production-ready or at GitKraken parity. The strongest areas are local status/staging/commit/history/branch/merge/rebase/stash/tag/worktree primitives and an initial AI service. The main gaps are product depth: credential management, hosted-provider workflows, advanced graph UX, partial hunk staging, robust diff modes, multi-repo workspace semantics, crash/updater/telemetry controls, and performance instrumentation.

This audit added backend contracts for previously missing core Git workflows: clone, init, reflog, submodule status/update, and bisect lifecycle. These are intentionally implemented in Rust/Tauri IPC so frontend screens can remain thin.

## Feature comparison table
| Feature | GitPilot | GitKraken | Status | Gap severity |
|---|---|---|---|---|
| Open existing repo | Yes | Yes | Usable | P3 |
| Clone repository | Backend IPC added; UI pending | Yes | Partial | P1 |
| Init repository | Backend IPC added; UI pending | Yes | Partial | P1 |
| Status/stage/commit/amend | Yes | Yes | Usable | P2 |
| Partial hunk staging | Limited/no dedicated hunk model | Yes | Missing | P0 |
| Branch create/delete/switch | Yes | Yes | Usable | P2 |
| Merge/rebase/cherry-pick/revert/reset | Yes, porcelain-backed | Yes | Partial UX | P1 |
| Interactive rebase squash/fixup | Backend exists; UX basic | Yes | Partial | P1 |
| Stash manager | Basic list/apply/pop/drop/rename | Yes | Partial | P2 |
| Tags | Yes | Yes | Usable | P2 |
| Reflog | Backend IPC added; UI pending | Yes/undo history | Partial | P1 |
| Detached HEAD | Checkout commit exists | Yes | Partial | P2 |
| Worktrees | Backend exists | Limited | Competitive | P2 |
| Submodules | Backend IPC added; UI pending | Yes | Partial | P1 |
| Bisect | Backend IPC added; UI pending | No/limited | GitPilot+ potential | P2 |
| Commit graph | Basic graph string/rendering | Advanced lanes/labels | Gap | P1 |
| Search/filter/blame/file history | Search and blame exist | Yes | Partial | P1 |
| GitHub/GitLab/Bitbucket PRs | Not implemented | Yes | Missing | P0 |
| SSH/HTTPS auth and token vault | Relies on system git; no vault UI | Yes | Missing | P0 |
| Command palette/shortcuts | Limited settings type only | Yes | Missing | P1 |
| Multi-repo workspace | Recent repos only | Yes | Missing | P1 |
| Conflict resolver | Marker parser/editor exists | Visual 3-way | Partial | P1 |
| AI commit/diff/conflict help | Initial AI commands | Limited | Differentiator | P2 |
| Updater/crash/telemetry toggle | Not productionized | Yes | Missing | P0 |

## Missing features list
### Core Git
- Partial hunk and line staging with reverse patch safety.
- Multi-commit cherry-pick and queued revert flows.
- First-class reflog browser and one-click undo built on reflog/reset/revert.
- UI for clone/init/submodule/bisect commands added in this patch.
- Visual 3-way conflict resolver with base/current/incoming panes.

### Visualization
- Commit graph lane assignment, stable lane colors, branch/tag pills, avatars, selection virtualization, and drag/drop branch/rebase operations.

### Productivity
- Global command palette (`Ctrl+Shift+P`), fuzzy repo/action search, configurable keyboard shortcuts, and quick actions.

### Repository management
- Workspace model with repo groups, favorites, pinned/recent repos, per-repo settings, and tab persistence.

### AI
- PR summary generation and hosted-provider posting.
- Diff risk summarization and review checklists.
- Conflict explanation that references parsed conflict blocks.

### Enterprise
- Secure credential/token storage using OS keychain plugins.
- Crash logging with opt-in telemetry toggle.
- Signed auto-updater and release channels.

## Bug list / risks
- `git pull` could not be run in this checkout because the current branch has no configured remote tracking branch.
- CI intentionally builds and uploads Linux, Windows, and macOS desktop bundles on every PR so reviewers can download platform artifacts; the tradeoff is slower validation until a separate opt-in fast-check workflow exists.
- Git operations are porcelain subprocess calls; acceptable for breadth, but long-running commands need cancellation/progress streaming.
- Recent repository storage is flat and not a workspace schema.
- AI key is represented in settings and should move to secure storage before production.

## Refactor proposals
1. Introduce a typed Git operation layer that owns argument validation, command execution, cancellation, progress, and audit events.
2. Split Tauri commands into fast read commands and long-running task commands with progress events.
3. Add a frontend feature shell: command palette, workspace navigator, repository tabs, settings, and hosted-provider account area.
4. Replace graph string rendering with a lane-layout engine that can be benchmarked independently.
5. Add integration tests that create temporary repositories and exercise each command.

## Roadmap
### MVP
- Wire clone/init/reflog/submodule/bisect IPC into UI.
- Add partial hunk staging and visual conflict improvements.
- Add command palette and keyboard shortcuts.

### Beta
- Multi-repo workspaces, pinned/favorites, graph lanes, file history, PR checkout, credential vault.
- Add smoke/integration tests and performance baselines.

### Production
- Signed updater, crash logging, telemetry toggle, hardened secret storage, provider integrations, release channels.
- Keep PR artifact builds for reviewer access, then add an optional fast-check workflow or label-gated packaging path if CI minutes become a bottleneck.

### GitKraken+ parity
- Drag/drop rebase/branch operations, advanced PR review, author avatars, AI summaries, bisect UI, and repository insights dashboards.
138 changes: 138 additions & 0 deletions src-tauri/src/commands/maintenance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use crate::{
models::git::{BisectState, GitCommandOutput, GitError, ReflogEntry, SubmoduleInfo},
services::git_service,
};
use std::path::Path;

#[tauri::command]
pub fn list_reflog(repo_path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>, GitError> {
let count = limit.unwrap_or(100).clamp(1, 1000).to_string();
let pretty = "%gd%x1f%H%x1f%gs";
let max_count = format!("--max-count={count}");
let format = format!("--format={pretty}");
let out = git_service::git_text(&repo_path, &["reflog", &max_count, &format])?;
Ok(out
.lines()
.filter_map(|line| {
let mut parts = line.splitn(3, '\u{1f}');
Some(ReflogEntry {
selector: parts.next()?.to_string(),
commit: parts.next()?.to_string(),
subject: parts.next().unwrap_or_default().to_string(),
})
})
.collect())
}

#[tauri::command]
pub fn list_submodules(repo_path: String) -> Result<Vec<SubmoduleInfo>, GitError> {
let out = git_service::git(&repo_path, &["submodule", "status", "--recursive"])?;
if !out.success && out.stderr.contains("no submodule mapping") {
return Ok(vec![]);
}
if !out.success {
return Err(GitError::new(
"GIT_COMMAND_FAILED",
out.stderr.clone(),
out.stderr,
));
}
Ok(out
.stdout
.lines()
.filter_map(|line| {
let status = line.chars().next().unwrap_or(' ').to_string();
let rest = line.get(1..)?.trim();
let mut parts = rest.split_whitespace();
let commit = parts.next()?.to_string();
let path = parts.next()?.to_string();
let branch = parts
.next()
.map(|s| s.trim_matches(|c| c == '(' || c == ')').to_string());
Some(SubmoduleInfo {
path,
commit,
branch,
status,
})
})
.collect())
}

#[tauri::command]
pub fn update_submodules(
repo_path: String,
init: bool,
recursive: bool,
) -> Result<GitCommandOutput, GitError> {
let mut args = vec!["submodule", "update"];
if init {
args.push("--init");
}
if recursive {
args.push("--recursive");
}
git_service::git_checked(&repo_path, &args)
}

#[tauri::command]
pub fn start_bisect(
repo_path: String,
bad: String,
good: String,
) -> Result<GitCommandOutput, GitError> {
if bad.trim().is_empty() || good.trim().is_empty() {
return Err(GitError::new(
"INVALID_BISECT_RANGE",
"Both bad and good revisions are required",
"",
));
}
git_service::git_checked(&repo_path, &["bisect", "start", bad.trim(), good.trim()])
}

#[tauri::command]
pub fn mark_bisect(repo_path: String, verdict: String) -> Result<GitCommandOutput, GitError> {
match verdict.as_str() {
"good" | "bad" | "skip" => {
git_service::git_checked(&repo_path, &["bisect", verdict.as_str()])
}
_ => Err(GitError::new(
"INVALID_BISECT_VERDICT",
"Bisect verdict must be good, bad, or skip",
"",
)),
}
}

#[tauri::command]
pub fn reset_bisect(repo_path: String) -> Result<GitCommandOutput, GitError> {
git_service::git_checked(&repo_path, &["bisect", "reset"])
}

#[tauri::command]
pub fn get_bisect_state(repo_path: String) -> Result<BisectState, GitError> {
let git_dir = git_service::git_text(&repo_path, &["rev-parse", "--git-dir"])?;
let in_progress = Path::new(&repo_path)
.join(git_dir.trim())
.join("BISECT_LOG")
.exists();
let current = git_service::git_text(&repo_path, &["rev-parse", "--short", "HEAD"])
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let log = if in_progress {
git_service::git_text(&repo_path, &["bisect", "log"])
.unwrap_or_default()
.lines()
.map(str::to_string)
.collect()
} else {
vec![]
};
Ok(BisectState {
in_progress,
current,
log,
})
}
1 change: 1 addition & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod branch;
pub mod commit;
pub mod diff;
pub mod history;
pub mod maintenance;
pub mod merge;
pub mod rebase;
pub mod remote;
Expand Down
74 changes: 74 additions & 0 deletions src-tauri/src/commands/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,80 @@ pub fn open_repository(path: String) -> Result<RepositoryInfo, GitError> {
current_branch: branch,
})
}

#[tauri::command]
pub fn init_repository(
path: String,
initial_branch: Option<String>,
) -> Result<RepositoryInfo, GitError> {
if !Path::new(&path).is_dir() {
return Err(GitError::new(
"INVALID_PATH",
"Repository path must be an existing directory",
"",
));
}
let mut args = vec!["init"];
if let Some(branch) = initial_branch.as_deref().filter(|b| !b.trim().is_empty()) {
args.push("--initial-branch");
args.push(branch.trim());
}
git_service::git_checked(&path, &args)?;
open_repository(path)
}
#[tauri::command]
pub fn clone_repository(
url: String,
destination: String,
branch: Option<String>,
depth: Option<u32>,
) -> Result<RepositoryInfo, GitError> {
if url.trim().is_empty() {
return Err(GitError::new("INVALID_REMOTE", "Clone URL is required", ""));
}
if destination.trim().is_empty() {
return Err(GitError::new(
"INVALID_PATH",
"Destination path is required",
"",
));
}
let destination_path = Path::new(&destination);
if destination_path.exists()
&& destination_path
.read_dir()
.map(|mut e| e.next().is_some())
.unwrap_or(true)
{
return Err(GitError::new(
"DESTINATION_NOT_EMPTY",
"Clone destination must be empty or not exist",
"",
));
}
let parent = destination_path.parent().unwrap_or_else(|| Path::new("."));
let leaf = destination_path
.file_name()
.map(|v| v.to_string_lossy().to_string())
.ok_or_else(|| {
GitError::new("INVALID_PATH", "Destination must include a folder name", "")
})?;
let mut owned = vec!["clone".to_string()];
if let Some(branch) = branch.filter(|b| !b.trim().is_empty()) {
owned.push("--branch".into());
owned.push(branch.trim().into());
}
if let Some(depth) = depth.filter(|d| *d > 0) {
owned.push("--depth".into());
owned.push(depth.to_string());
}
owned.push(url.trim().into());
owned.push(leaf);
let refs: Vec<&str> = owned.iter().map(String::as_str).collect();
git_service::git_checked(parent.to_string_lossy().as_ref(), &refs)?;
open_repository(destination)
}

#[tauri::command]
pub fn list_recent_repositories() -> Result<Vec<String>, String> {
Ok(config_service::load()?.recent_repositories)
Expand Down
11 changes: 10 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
commands::repository::validate_repository,
commands::repository::open_repository,
commands::repository::init_repository,
commands::repository::clone_repository,
commands::repository::list_recent_repositories,
commands::repository::save_recent_repository,
commands::repository::remove_recent_repository,
Expand Down Expand Up @@ -77,7 +79,14 @@ pub fn run() {
commands::ai::suggest_branch_name,
commands::ai::resolve_conflict_block,
commands::settings::get_settings,
commands::settings::save_settings
commands::settings::save_settings,
commands::maintenance::list_reflog,
commands::maintenance::list_submodules,
commands::maintenance::update_submodules,
commands::maintenance::start_bisect,
commands::maintenance::mark_bisect,
commands::maintenance::reset_bisect,
commands::maintenance::get_bisect_state
])
.run(tauri::generate_context!())
.expect("failed to run GitPilot");
Expand Down
23 changes: 23 additions & 0 deletions src-tauri/src/models/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,26 @@ pub struct SearchResult {
pub subtitle: String,
pub target: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReflogEntry {
pub selector: String,
pub commit: String,
pub subject: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubmoduleInfo {
pub path: String,
pub commit: String,
pub branch: Option<String>,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BisectState {
pub in_progress: bool,
pub current: Option<String>,
pub log: Vec<String>,
}
Loading
Loading