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
17 changes: 9 additions & 8 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ tokio = { version = "1", features = ["fs", "net", "io-util", "process", "rt", "s
uuid = { version = "1", features = ["v4"] }
tauri-plugin-dialog = "2"
git2 = "0.20.3"
base64 = "0.22"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
ignore = "0.4.25"
portable-pty = "0.8"
Expand Down
242 changes: 188 additions & 54 deletions src-tauri/src/git.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use std::fs;
use std::path::{Path, PathBuf};

use base64::{engine::general_purpose::STANDARD, Engine as _};
use git2::{BranchType, DiffOptions, Repository, Sort, Status, StatusOptions};
use serde_json::json;
use tauri::State;
use tokio::process::Command;

use crate::git_utils::{
checkout_branch, commit_to_entry, diff_patch_to_string, diff_stats_for_path,
list_git_roots as scan_git_roots, parse_github_repo, resolve_git_root,
image_mime_type, list_git_roots as scan_git_roots, parse_github_repo, resolve_git_root,
};
use crate::state::AppState;
use crate::types::{
Expand All @@ -18,6 +20,30 @@ use crate::types::{
use crate::utils::normalize_git_path;

const INDEX_SKIP_WORKTREE_FLAG: u16 = 0x4000;
const MAX_IMAGE_BYTES: usize = 10 * 1024 * 1024;

fn encode_image_base64(data: &[u8]) -> Option<String> {
if data.len() > MAX_IMAGE_BYTES {
return None;
}
Some(STANDARD.encode(data))
}

fn blob_to_base64(blob: git2::Blob) -> Option<String> {
if blob.size() > MAX_IMAGE_BYTES {
return None;
}
encode_image_base64(blob.content())
}

fn read_image_base64(path: &Path) -> Option<String> {
let metadata = fs::metadata(path).ok()?;
if metadata.len() > MAX_IMAGE_BYTES as u64 {
return None;
}
let data = fs::read(path).ok()?;
encode_image_base64(&data)
}

async fn run_git_command(repo_root: &Path, args: &[&str]) -> Result<(), String> {
let output = Command::new("git")
Expand Down Expand Up @@ -392,6 +418,7 @@ pub(crate) async fn get_git_status(
.get(&workspace_id)
.ok_or("workspace not found")?
.clone();
drop(workspaces);

let repo_root = resolve_git_root(&entry)?;
let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -725,57 +752,113 @@ pub(crate) async fn get_git_diffs(
.clone();

let repo_root = resolve_git_root(&entry)?;
let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?;
let head_tree = repo
.head()
.ok()
.and_then(|head| head.peel_to_tree().ok());

let mut options = DiffOptions::new();
options
.include_untracked(true)
.recurse_untracked_dirs(true)
.show_untracked_content(true);
tokio::task::spawn_blocking(move || {
let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?;
let head_tree = repo
.head()
.ok()
.and_then(|head| head.peel_to_tree().ok());

let mut options = DiffOptions::new();
options
.include_untracked(true)
.recurse_untracked_dirs(true)
.show_untracked_content(true);

let diff = match head_tree.as_ref() {
Some(tree) => repo
.diff_tree_to_workdir_with_index(Some(tree), Some(&mut options))
.map_err(|e| e.to_string())?,
None => repo
.diff_tree_to_workdir_with_index(None, Some(&mut options))
.map_err(|e| e.to_string())?,
};

let diff = match head_tree.as_ref() {
Some(tree) => repo
.diff_tree_to_workdir_with_index(Some(tree), Some(&mut options))
.map_err(|e| e.to_string())?,
None => repo
.diff_tree_to_workdir_with_index(None, Some(&mut options))
.map_err(|e| e.to_string())?,
};
let mut results = Vec::new();
for (index, delta) in diff.deltas().enumerate() {
let old_path = delta.old_file().path();
let new_path = delta.new_file().path();
let display_path = new_path.or(old_path);
let Some(display_path) = display_path else {
continue;
};
let old_path_str = old_path.map(|path| path.to_string_lossy());
let new_path_str = new_path.map(|path| path.to_string_lossy());
let display_path_str = display_path.to_string_lossy();
let normalized_path = normalize_git_path(&display_path_str);
let old_image_mime = old_path_str.as_deref().and_then(image_mime_type);
let new_image_mime = new_path_str.as_deref().and_then(image_mime_type);
let is_image = old_image_mime.is_some() || new_image_mime.is_some();

if is_image {
let is_deleted = delta.status() == git2::Delta::Deleted;
let is_added = delta.status() == git2::Delta::Added;

let old_image_data = if !is_added && old_image_mime.is_some() {
head_tree
.as_ref()
.and_then(|tree| old_path.and_then(|path| tree.get_path(path).ok()))
.and_then(|entry| repo.find_blob(entry.id()).ok())
.and_then(blob_to_base64)
} else {
None
};

let new_image_data = if !is_deleted && new_image_mime.is_some() {
match new_path {
Some(path) => {
let full_path = repo_root.join(path);
read_image_base64(&full_path)
}
None => None,
}
} else {
None
};

results.push(GitFileDiff {
path: normalized_path,
diff: String::new(),
is_binary: true,
is_image: true,
old_image_data,
new_image_data,
old_image_mime: old_image_mime.map(str::to_string),
new_image_mime: new_image_mime.map(str::to_string),
});
continue;
}

let mut results = Vec::new();
for (index, delta) in diff.deltas().enumerate() {
let path = delta
.new_file()
.path()
.or_else(|| delta.old_file().path());
let Some(path) = path else {
continue;
};
let patch = match git2::Patch::from_diff(&diff, index) {
Ok(patch) => patch,
Err(_) => continue,
};
let Some(mut patch) = patch else {
continue;
};
let content = match diff_patch_to_string(&mut patch) {
Ok(content) => content,
Err(_) => continue,
};
if content.trim().is_empty() {
continue;
let patch = match git2::Patch::from_diff(&diff, index) {
Ok(patch) => patch,
Err(_) => continue,
};
let Some(mut patch) = patch else {
continue;
};
let content = match diff_patch_to_string(&mut patch) {
Ok(content) => content,
Err(_) => continue,
};
if content.trim().is_empty() {
continue;
}
results.push(GitFileDiff {
path: normalized_path,
diff: content,
is_binary: false,
is_image: false,
old_image_data: None,
new_image_data: None,
old_image_mime: None,
new_image_mime: None,
});
}
results.push(GitFileDiff {
path: normalize_git_path(path.to_string_lossy().as_ref()),
diff: content,
});
}

Ok(results)
Ok(results)
})
.await
.map_err(|e| e.to_string())?
}

#[tauri::command]
Expand All @@ -789,6 +872,7 @@ pub(crate) async fn get_git_log(
.get(&workspace_id)
.ok_or("workspace not found")?
.clone();
drop(workspaces);

let repo_root = resolve_git_root(&entry)?;
let repo = Repository::open(&repo_root).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -915,13 +999,57 @@ pub(crate) async fn get_git_commit_diff(

let mut results = Vec::new();
for (index, delta) in diff.deltas().enumerate() {
let path = delta
.new_file()
.path()
.or_else(|| delta.old_file().path());
let Some(path) = path else {
let old_path = delta.old_file().path();
let new_path = delta.new_file().path();
let display_path = new_path.or(old_path);
let Some(display_path) = display_path else {
continue;
};
let old_path_str = old_path.map(|path| path.to_string_lossy());
let new_path_str = new_path.map(|path| path.to_string_lossy());
let display_path_str = display_path.to_string_lossy();
let normalized_path = normalize_git_path(&display_path_str);
let old_image_mime = old_path_str.as_deref().and_then(image_mime_type);
let new_image_mime = new_path_str.as_deref().and_then(image_mime_type);
let is_image = old_image_mime.is_some() || new_image_mime.is_some();

if is_image {
let is_deleted = delta.status() == git2::Delta::Deleted;
let is_added = delta.status() == git2::Delta::Added;

let old_image_data = if !is_added && old_image_mime.is_some() {
parent_tree
.as_ref()
.and_then(|tree| old_path.and_then(|path| tree.get_path(path).ok()))
.and_then(|entry| repo.find_blob(entry.id()).ok())
.and_then(blob_to_base64)
} else {
None
};

let new_image_data = if !is_deleted && new_image_mime.is_some() {
new_path
.and_then(|path| commit_tree.get_path(path).ok())
.and_then(|entry| repo.find_blob(entry.id()).ok())
.and_then(blob_to_base64)
} else {
None
};

results.push(GitCommitDiff {
path: normalized_path,
status: status_for_delta(delta.status()).to_string(),
diff: String::new(),
is_binary: true,
is_image: true,
old_image_data,
new_image_data,
old_image_mime: old_image_mime.map(str::to_string),
new_image_mime: new_image_mime.map(str::to_string),
});
continue;
}

let patch = match git2::Patch::from_diff(&diff, index) {
Ok(patch) => patch,
Err(_) => continue,
Expand All @@ -937,9 +1065,15 @@ pub(crate) async fn get_git_commit_diff(
continue;
}
results.push(GitCommitDiff {
path: normalize_git_path(path.to_string_lossy().as_ref()),
path: normalized_path,
status: status_for_delta(delta.status()).to_string(),
diff: content,
is_binary: false,
is_image: false,
old_image_data: None,
new_image_data: None,
old_image_mime: None,
new_image_mime: None,
});
}

Expand Down
Loading