Improve Git integration: Add dirty worktree validation for pushes, enhance initial repo setup by auto-generating .gitignore rules, and refactor Gitea API error handling.
This commit is contained in:
10
renderer.js
10
renderer.js
@@ -59,8 +59,16 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
pushBtn.textContent = 'Pushing…';
|
pushBtn.textContent = 'Pushing…';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const hasUncommittedFiles = await window.electronAPI.hasDiffs(selected);
|
||||||
|
if (hasUncommittedFiles) {
|
||||||
|
const confirmed = confirm(
|
||||||
|
'This repository contains uncommitted or untracked files.\n\n' +
|
||||||
|
'Only committed files will be pushed. Continue anyway?'
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
// send the folder‐path to main via a new IPC channel 'push-to-gitea'
|
// send the folder‐path to main via a new IPC channel 'push-to-gitea'
|
||||||
const result = await window.electronAPI.pushToGitea(selected.path);
|
const result = await window.electronAPI.pushToGitea(selected.path, hasUncommittedFiles);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert('✔ Pushed successfully to Gitea: ' + result.repoUrl);
|
alert('✔ Pushed successfully to Gitea: ' + result.repoUrl);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -623,7 +623,8 @@ fn init_git_repo_internal(folder: &str) -> CommandResult<()> {
|
|||||||
if !Path::new(folder).join(".git").exists() {
|
if !Path::new(folder).join(".git").exists() {
|
||||||
run_git(folder, &["init"])?;
|
run_git(folder, &["init"])?;
|
||||||
ensure_gitignore_defaults(folder)?;
|
ensure_gitignore_defaults(folder)?;
|
||||||
run_git(folder, &["add", ".gitignore"])?;
|
update_gitignore_from_existing_project(folder)?;
|
||||||
|
run_git(folder, &["add", "-A"])?;
|
||||||
run_git(folder, &["commit", "--allow-empty", "-m", "initial commit"])?;
|
run_git(folder, &["commit", "--allow-empty", "-m", "initial commit"])?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1319,6 +1320,47 @@ fn file_name_matches_ignore(name: &str, pattern: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_gitignore_from_existing_project(folder_path: &str) -> CommandResult<()> {
|
||||||
|
let mut stack = vec![PathBuf::from(folder_path)];
|
||||||
|
let mut patterns = HashSet::new();
|
||||||
|
|
||||||
|
while let Some(current) = stack.pop() {
|
||||||
|
let entries = fs::read_dir(¤t).map_err(|e| e.to_string())?;
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.map_err(|e| e.to_string())?;
|
||||||
|
let path = entry.path();
|
||||||
|
let file_type = entry.file_type().map_err(|e| e.to_string())?;
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if let Some(pattern) = tauri_build_ignore_for_path(folder_path, &path) {
|
||||||
|
patterns.insert(pattern.to_string());
|
||||||
|
}
|
||||||
|
let matched_patterns = IGNORED_NAMES
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|pattern| file_name_matches_ignore(&name, pattern))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for pattern in &matched_patterns {
|
||||||
|
patterns.insert((*pattern).to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if file_type.is_dir()
|
||||||
|
&& matched_patterns.is_empty()
|
||||||
|
&& !should_ignore_path(folder_path, &path)
|
||||||
|
{
|
||||||
|
stack.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut patterns = patterns.into_iter().collect::<Vec<_>>();
|
||||||
|
patterns.sort();
|
||||||
|
for pattern in patterns {
|
||||||
|
ensure_in_gitignore(folder_path, &pattern)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn exceeds_file_limit(folder_path: &str, limit: usize) -> bool {
|
fn exceeds_file_limit(folder_path: &str, limit: usize) -> bool {
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
let mut stack = vec![PathBuf::from(folder_path)];
|
let mut stack = vec![PathBuf::from(folder_path)];
|
||||||
@@ -3322,11 +3364,56 @@ fn generate_repo_description(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn slugify_repository_name(name: &str) -> String {
|
||||||
|
let mut slug = String::new();
|
||||||
|
let mut needs_separator = false;
|
||||||
|
for character in name.chars() {
|
||||||
|
if character.is_ascii_alphanumeric() {
|
||||||
|
if needs_separator && !slug.is_empty() {
|
||||||
|
slug.push('-');
|
||||||
|
}
|
||||||
|
slug.push(character.to_ascii_lowercase());
|
||||||
|
needs_separator = false;
|
||||||
|
} else {
|
||||||
|
needs_separator = !slug.is_empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slug.is_empty() {
|
||||||
|
"repository".to_string()
|
||||||
|
} else {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gitea_response_error(context: &str, response: reqwest::blocking::Response) -> String {
|
||||||
|
let status = response.status();
|
||||||
|
let body = response.text().unwrap_or_default();
|
||||||
|
let detail = serde_json::from_str::<Value>(&body)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| {
|
||||||
|
let message = value.get("message").and_then(Value::as_str);
|
||||||
|
let errors = value.get("errors").filter(|errors| !errors.is_null());
|
||||||
|
match (message, errors) {
|
||||||
|
(Some(message), Some(errors)) => Some(format!("{message}; {errors}")),
|
||||||
|
(Some(message), None) => Some(message.to_string()),
|
||||||
|
(None, Some(errors)) => Some(errors.to_string()),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_else(|| (!body.trim().is_empty()).then(|| body.trim().to_string()))
|
||||||
|
.unwrap_or_else(|| "Gitea returned no error details".to_string());
|
||||||
|
format!(
|
||||||
|
"{context} failed ({status}): {}",
|
||||||
|
truncate_text(detail, 1000)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn push_to_gitea(
|
fn push_to_gitea(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
state: tauri::State<'_, AppState>,
|
state: tauri::State<'_, AppState>,
|
||||||
folder_path: String,
|
folder_path: String,
|
||||||
|
allow_dirty: bool,
|
||||||
) -> CommandResult<Value> {
|
) -> CommandResult<Value> {
|
||||||
let token = state
|
let token = state
|
||||||
.store
|
.store
|
||||||
@@ -3339,11 +3426,19 @@ fn push_to_gitea(
|
|||||||
json!({ "success": false, "error": "No Gitea API token configured – open Settings and enter it first" }),
|
json!({ "success": false, "error": "No Gitea API token configured – open Settings and enter it first" }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if has_status_changes(&git_status(&folder_path)?) && !allow_dirty {
|
||||||
|
return Ok(json!({
|
||||||
|
"success": false,
|
||||||
|
"code": "DIRTY_WORKTREE",
|
||||||
|
"error": "The repository contains uncommitted or untracked files. Confirm the warning before pushing."
|
||||||
|
}));
|
||||||
|
}
|
||||||
let result = (|| -> CommandResult<Value> {
|
let result = (|| -> CommandResult<Value> {
|
||||||
let repo_name = Path::new(&folder_path)
|
let folder_name = Path::new(&folder_path)
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|s| s.to_str())
|
.and_then(|s| s.to_str())
|
||||||
.ok_or_else(|| "invalid repository path".to_string())?;
|
.ok_or_else(|| "invalid repository path".to_string())?;
|
||||||
|
let repo_name = slugify_repository_name(folder_name);
|
||||||
let base = "https://giers10.uber.space/api/v1";
|
let base = "https://giers10.uber.space/api/v1";
|
||||||
let description = generate_repo_description(&app, &state, &folder_path)?;
|
let description = generate_repo_description(&app, &state, &folder_path)?;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
@@ -3353,7 +3448,7 @@ fn push_to_gitea(
|
|||||||
.send()
|
.send()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
if !user_resp.status().is_success() {
|
if !user_resp.status().is_success() {
|
||||||
return Err(format!("/user request failed: {}", user_resp.status()));
|
return Err(gitea_response_error("Gitea user request", user_resp));
|
||||||
}
|
}
|
||||||
let user: Value = user_resp.json().map_err(|e| e.to_string())?;
|
let user: Value = user_resp.json().map_err(|e| e.to_string())?;
|
||||||
let username = user
|
let username = user
|
||||||
@@ -3366,7 +3461,7 @@ fn push_to_gitea(
|
|||||||
.send()
|
.send()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let repo_url = if check.status().as_u16() == 404 {
|
let repo_url = if check.status().as_u16() == 404 {
|
||||||
let created: Value = client
|
let created_response = client
|
||||||
.post(format!("{base}/user/repos"))
|
.post(format!("{base}/user/repos"))
|
||||||
.header("Authorization", format!("token {token}"))
|
.header("Authorization", format!("token {token}"))
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
@@ -3376,34 +3471,40 @@ fn push_to_gitea(
|
|||||||
"auto_init": false
|
"auto_init": false
|
||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.json()
|
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
if !created_response.status().is_success() {
|
||||||
|
return Err(gitea_response_error(
|
||||||
|
"Creating the Gitea repository",
|
||||||
|
created_response,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let created: Value = created_response.json().map_err(|e| e.to_string())?;
|
||||||
created
|
created
|
||||||
.get("clone_url")
|
.get("clone_url")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or_else(|| "Gitea create response missing clone_url".to_string())?
|
.ok_or_else(|| "Gitea create response missing clone_url".to_string())?
|
||||||
.to_string()
|
.to_string()
|
||||||
} else if check.status().is_success() {
|
} else if check.status().is_success() {
|
||||||
let _ = client
|
let update_response = client
|
||||||
.patch(format!("{base}/repos/{username}/{repo_name}"))
|
.patch(format!("{base}/repos/{username}/{repo_name}"))
|
||||||
.header("Authorization", format!("token {token}"))
|
.header("Authorization", format!("token {token}"))
|
||||||
.json(&json!({ "description": description }))
|
.json(&json!({ "description": description }))
|
||||||
.send();
|
|
||||||
let existing: Value = client
|
|
||||||
.get(format!("{base}/repos/{username}/{repo_name}"))
|
|
||||||
.header("Authorization", format!("token {token}"))
|
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.json()
|
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
if !update_response.status().is_success() {
|
||||||
|
return Err(gitea_response_error(
|
||||||
|
"Updating the Gitea repository description",
|
||||||
|
update_response,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let existing: Value = check.json().map_err(|e| e.to_string())?;
|
||||||
existing
|
existing
|
||||||
.get("clone_url")
|
.get("clone_url")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or_else(|| "Gitea repo response missing clone_url".to_string())?
|
.ok_or_else(|| "Gitea repo response missing clone_url".to_string())?
|
||||||
.to_string()
|
.to_string()
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Error checking repo: {}", check.status()));
|
return Err(gitea_response_error("Checking the Gitea repository", check));
|
||||||
};
|
};
|
||||||
let _ = run_git(&folder_path, &["remote", "remove", "origin"]);
|
let _ = run_git(&folder_path, &["remote", "remove", "origin"]);
|
||||||
run_git(&folder_path, &["remote", "add", "origin", &repo_url])?;
|
run_git(&folder_path, &["remote", "add", "origin", &repo_url])?;
|
||||||
@@ -3572,6 +3673,30 @@ fn main() {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repository_names_are_slugified_for_gitea() {
|
||||||
|
assert_eq!(
|
||||||
|
slugify_repository_name("EP-133 Sample Tool Offline"),
|
||||||
|
"ep-133-sample-tool-offline"
|
||||||
|
);
|
||||||
|
assert_eq!(slugify_repository_name(" Fragile___App "), "fragile-app");
|
||||||
|
assert_eq!(slugify_repository_name("☃"), "repository");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_project_ignore_rules_are_written_before_staging() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
fs::write(temp.path().join(".DS_Store"), "metadata").unwrap();
|
||||||
|
fs::create_dir(temp.path().join("Release.app")).unwrap();
|
||||||
|
fs::write(temp.path().join("Release.app").join("binary"), "binary").unwrap();
|
||||||
|
|
||||||
|
update_gitignore_from_existing_project(temp.path().to_str().unwrap()).unwrap();
|
||||||
|
|
||||||
|
let gitignore = fs::read_to_string(temp.path().join(".gitignore")).unwrap();
|
||||||
|
assert!(gitignore.lines().any(|line| line == ".DS_Store"));
|
||||||
|
assert!(gitignore.lines().any(|line| line == "*.app"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reword_hashes_are_processed_newest_first() {
|
fn reword_hashes_are_processed_newest_first() {
|
||||||
let all_commits = vec![
|
let all_commits = vec![
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
hasReadme: folderPath => invoke('has_readme', { folderPath }),
|
hasReadme: folderPath => invoke('has_readme', { folderPath }),
|
||||||
generateReadme: folderPath => invoke('generate_readme', { folderPath }),
|
generateReadme: folderPath => invoke('generate_readme', { folderPath }),
|
||||||
squashCommits: folderPath => invoke('squash_commits', { folderPath }),
|
squashCommits: folderPath => invoke('squash_commits', { folderPath }),
|
||||||
pushToGitea: folderPath => invoke('push_to_gitea', { folderPath }),
|
pushToGitea: (folderPath, allowDirty = false) => invoke('push_to_gitea', { folderPath, allowDirty }),
|
||||||
initRepo: folderPath => invoke('init_repo', { folderPath }),
|
initRepo: folderPath => invoke('init_repo', { folderPath }),
|
||||||
triggerRewriteNow: folderPath => invoke('trigger_rewrite_now', { folderPath })
|
triggerRewriteNow: folderPath => invoke('trigger_rewrite_now', { folderPath })
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user