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
196 changes: 165 additions & 31 deletions src-tauri/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,63 @@ use crate::types::{
};
use crate::utils::normalize_git_path;

async fn run_git_command(repo_root: &Path, args: &[&str]) -> Result<(), String> {
let output = Command::new("git")
.args(args)
.current_dir(repo_root)
.output()
.await
.map_err(|e| format!("Failed to run git: {e}"))?;

if output.status.success() {
return Ok(());
}

let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let detail = if stderr.trim().is_empty() {
stdout.trim()
} else {
stderr.trim()
};
if detail.is_empty() {
return Err("Git command failed.".to_string());
}
Err(detail.to_string())
}

fn status_for_index(status: Status) -> Option<&'static str> {
if status.contains(Status::INDEX_NEW) {
Some("A")
} else if status.contains(Status::INDEX_MODIFIED) {
Some("M")
} else if status.contains(Status::INDEX_DELETED) {
Some("D")
} else if status.contains(Status::INDEX_RENAMED) {
Some("R")
} else if status.contains(Status::INDEX_TYPECHANGE) {
Some("T")
} else {
None
}
}

fn status_for_workdir(status: Status) -> Option<&'static str> {
if status.contains(Status::WT_NEW) {
Some("A")
} else if status.contains(Status::WT_MODIFIED) {
Some("M")
} else if status.contains(Status::WT_DELETED) {
Some("D")
} else if status.contains(Status::WT_RENAMED) {
Some("R")
} else if status.contains(Status::WT_TYPECHANGE) {
Some("T")
} else {
None
}
}

fn github_repo_from_path(path: &Path) -> Result<String, String> {
let repo = Repository::open(path).map_err(|e| e.to_string())?;
let remotes = repo.remotes().map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -165,6 +222,8 @@ pub(crate) async fn get_git_status(
let head_tree = repo.head().ok().and_then(|head| head.peel_to_tree().ok());

let mut files = Vec::new();
let mut staged_files = Vec::new();
let mut unstaged_files = Vec::new();
let mut total_additions = 0i64;
let mut total_deletions = 0i64;
for entry in statuses.iter() {
Expand All @@ -173,21 +232,6 @@ pub(crate) async fn get_git_status(
continue;
}
let status = entry.status();
let status_str = if status.contains(Status::WT_NEW) || status.contains(Status::INDEX_NEW) {
"A"
} else if status.contains(Status::WT_MODIFIED) || status.contains(Status::INDEX_MODIFIED) {
"M"
} else if status.contains(Status::WT_DELETED) || status.contains(Status::INDEX_DELETED) {
"D"
} else if status.contains(Status::WT_RENAMED) || status.contains(Status::INDEX_RENAMED) {
"R"
} else if status.contains(Status::WT_TYPECHANGE)
|| status.contains(Status::INDEX_TYPECHANGE)
{
"T"
} else {
"--"
};
let normalized_path = normalize_git_path(path);
let include_index = status.intersects(
Status::INDEX_NEW
Expand All @@ -203,32 +247,122 @@ pub(crate) async fn get_git_status(
| Status::WT_RENAMED
| Status::WT_TYPECHANGE,
);
let (additions, deletions) = diff_stats_for_path(
&repo,
head_tree.as_ref(),
path,
include_index,
include_workdir,
)
.map_err(|e| e.to_string())?;
total_additions += additions;
total_deletions += deletions;
files.push(GitFileStatus {
path: normalized_path,
status: status_str.to_string(),
additions,
deletions,
});
let mut combined_additions = 0i64;
let mut combined_deletions = 0i64;

if include_index {
let (additions, deletions) =
diff_stats_for_path(&repo, head_tree.as_ref(), path, true, false)
.map_err(|e| e.to_string())?;
if let Some(status_str) = status_for_index(status) {
staged_files.push(GitFileStatus {
path: normalized_path.clone(),
status: status_str.to_string(),
additions,
deletions,
});
}
combined_additions += additions;
combined_deletions += deletions;
total_additions += additions;
total_deletions += deletions;
}

if include_workdir {
let (additions, deletions) =
diff_stats_for_path(&repo, head_tree.as_ref(), path, false, true)
.map_err(|e| e.to_string())?;
if let Some(status_str) = status_for_workdir(status) {
unstaged_files.push(GitFileStatus {
path: normalized_path.clone(),
status: status_str.to_string(),
additions,
deletions,
});
}
combined_additions += additions;
combined_deletions += deletions;
total_additions += additions;
total_deletions += deletions;
}

if include_index || include_workdir {
let status_str = status_for_workdir(status)
.or_else(|| status_for_index(status))
.unwrap_or("--");
files.push(GitFileStatus {
path: normalized_path,
status: status_str.to_string(),
additions: combined_additions,
deletions: combined_deletions,
});
}
}

Ok(json!({
"branchName": branch_name,
"files": files,
"stagedFiles": staged_files,
"unstagedFiles": unstaged_files,
"totalAdditions": total_additions,
"totalDeletions": total_deletions,
}))
}

#[tauri::command]
pub(crate) async fn stage_git_file(
workspace_id: String,
path: String,
state: State<'_, AppState>,
) -> Result<(), String> {
let workspaces = state.workspaces.lock().await;
let entry = workspaces
.get(&workspace_id)
.ok_or("workspace not found")?
.clone();

let repo_root = resolve_git_root(&entry)?;
run_git_command(&repo_root, &["add", "--", &path]).await
}

#[tauri::command]
pub(crate) async fn unstage_git_file(
workspace_id: String,
path: String,
state: State<'_, AppState>,
) -> Result<(), String> {
let workspaces = state.workspaces.lock().await;
let entry = workspaces
.get(&workspace_id)
.ok_or("workspace not found")?
.clone();

let repo_root = resolve_git_root(&entry)?;
run_git_command(&repo_root, &["restore", "--staged", "--", &path]).await
}

#[tauri::command]
pub(crate) async fn revert_git_file(
workspace_id: String,
path: String,
state: State<'_, AppState>,
) -> Result<(), String> {
let workspaces = state.workspaces.lock().await;
let entry = workspaces
.get(&workspace_id)
.ok_or("workspace not found")?
.clone();

let repo_root = resolve_git_root(&entry)?;
if run_git_command(&repo_root, &["restore", "--staged", "--worktree", "--", &path])
.await
.is_ok()
{
return Ok(());
}
run_git_command(&repo_root, &["clean", "-f", "--", &path]).await
}

#[tauri::command]
pub(crate) async fn list_git_roots(
workspace_id: String,
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ pub fn run() {
git::get_git_diffs,
git::get_git_log,
git::get_git_remote,
git::stage_git_file,
git::unstage_git_file,
git::revert_git_file,
git::get_github_issues,
git::get_github_pull_requests,
git::get_github_pull_request_diff,
Expand Down
37 changes: 35 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ import { useCopyThread } from "./features/threads/hooks/useCopyThread";
import { usePanelVisibility } from "./features/layout/hooks/usePanelVisibility";
import { useTerminalController } from "./features/terminal/hooks/useTerminalController";
import { playNotificationSound } from "./utils/notificationSounds";
import { pickWorkspacePath } from "./services/tauri";
import {
pickWorkspacePath,
revertGitFile,
stageGitFile,
unstageGitFile,
} from "./services/tauri";
import type {
AccessMode,
GitHubPullRequest,
Expand Down Expand Up @@ -305,7 +310,8 @@ function MainApp() {
const {
diffs: gitDiffs,
isLoading: isDiffLoading,
error: diffError
error: diffError,
refresh: refreshGitDiffs,
} = useGitDiffs(activeWorkspace, gitStatus.files, shouldLoadDiffs);
const {
entries: gitLogEntries,
Expand Down Expand Up @@ -387,6 +393,30 @@ function MainApp() {
await createBranch(name);
refreshGitStatus();
};
const handleStageGitFile = async (path: string) => {
if (!activeWorkspace) {
return;
}
await stageGitFile(activeWorkspace.id, path);
refreshGitStatus();
refreshGitDiffs();
};
const handleUnstageGitFile = async (path: string) => {
if (!activeWorkspace) {
return;
}
await unstageGitFile(activeWorkspace.id, path);
refreshGitStatus();
refreshGitDiffs();
};
const handleRevertGitFile = async (path: string) => {
if (!activeWorkspace) {
return;
}
await revertGitFile(activeWorkspace.id, path);
refreshGitStatus();
refreshGitDiffs();
};

const resolvedModel = selectedModel?.model ?? null;
const activeGitRoot = activeWorkspace?.settings.gitRoot ?? null;
Expand Down Expand Up @@ -1047,6 +1077,9 @@ function MainApp() {
void handleSetGitRoot(null);
},
onPickGitRoot: handlePickGitRoot,
onStageGitFile: handleStageGitFile,
onUnstageGitFile: handleUnstageGitFile,
onRevertGitFile: handleRevertGitFile,
gitDiffs: activeDiffs,
gitDiffLoading: activeDiffLoading,
gitDiffError: activeDiffError,
Expand Down
Loading