Merge branch 'opt/light_mode'
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m59s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 27m53s

This commit is contained in:
Christoph Brandau
2026-07-10 22:59:20 +02:00
9 changed files with 1389 additions and 106 deletions
+580 -7
View File
@@ -1,7 +1,9 @@
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
env,
ffi::{OsStr, OsString},
fs,
path::{Path, PathBuf},
process::{Command, Output, Stdio},
sync::{
@@ -170,6 +172,112 @@ pub struct GitSearchHit {
pub matches_added: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RebaseCommit {
pub hash: String,
pub short_hash: String,
pub summary: String,
pub author_name: String,
pub date: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RebaseAction {
Pick,
Reword,
Squash,
Fixup,
Drop,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct RebasePlanItem {
pub hash: String,
pub action: RebaseAction,
pub message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ReflogEntry {
pub hash: String,
pub short_hash: String,
pub selector: String,
pub action: String,
pub author_name: String,
pub date: String,
}
const SEQUENCE_EDITOR_PLAN_ENV: &str = "GITTY_SEQUENCE_EDITOR_PLAN";
const COMMIT_EDITOR_QUEUE_ENV: &str = "GITTY_COMMIT_EDITOR_QUEUE";
const REBASE_TODO_FILE: &str = "gitty-interactive-rebase-todo";
const REWORD_QUEUE_FILE: &str = "gitty-interactive-rebase-messages";
const SEQUENCE_HELPER_STEM: &str = ".gitty-sequence-editor";
const COMMIT_HELPER_STEM: &str = ".gitty-commit-editor";
pub fn run_sequence_editor_if_requested() -> Option<Result<(), String>> {
let executable = env::current_exe().ok()?;
let executable_name = executable.file_name()?.to_string_lossy();
let target = env::args_os()
.nth(1)
.ok_or_else(|| "Git did not provide a sequence-editor target path.".to_string());
if executable_name.contains("gitty-sequence-editor") {
let plan = env::var_os(SEQUENCE_EDITOR_PLAN_ENV)?;
return Some(target.and_then(|target| {
fs::copy(PathBuf::from(plan), PathBuf::from(target))
.map(|_| ())
.map_err(|err| format!("Could not write the interactive rebase plan: {err}"))
}));
}
if executable_name.contains("gitty-commit-editor") {
let queue = env::var_os(COMMIT_EDITOR_QUEUE_ENV)?;
return Some(
target.and_then(|target| apply_reword_message(Path::new(&queue), Path::new(&target))),
);
}
None
}
fn apply_reword_message(queue_path: &Path, message_path: &Path) -> Result<(), String> {
let queue = fs::read_to_string(queue_path)
.map_err(|err| format!("Could not read the reword queue: {err}"))?;
if queue.is_empty() {
return Ok(());
}
let current = fs::read_to_string(message_path)
.map_err(|err| format!("Could not read the commit message: {err}"))?;
let current_subject = current
.lines()
.map(str::trim)
.find(|line| !line.is_empty() && !line.starts_with('#'))
.unwrap_or("");
let mut remaining = Vec::new();
let mut replacement = None;
for record in queue.split('\x1e').filter(|record| !record.is_empty()) {
let Some((old, new)) = record.split_once('\x1f') else {
return Err("The reword queue is malformed.".to_string());
};
if replacement.is_none() && old == current_subject {
replacement = Some(new.to_string());
} else {
remaining.push(record);
}
}
if let Some(message) = replacement {
fs::write(message_path, format!("{message}\n"))
.map_err(|err| format!("Could not update the commit message: {err}"))?;
let mut next_queue = remaining.join("\x1e");
if !next_queue.is_empty() {
next_queue.push('\x1e');
}
fs::write(queue_path, next_queue)
.map_err(|err| format!("Could not update the reword queue: {err}"))?;
}
Ok(())
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct BranchInfo {
current_branch: Option<String>,
@@ -838,7 +946,11 @@ pub async fn unstage_files(path: String, files: Vec<String>) -> Result<GitStatus
}
#[tauri::command]
pub async fn restore_files(path: String, files: Vec<String>, staged: bool) -> Result<GitStatus, String> {
pub async fn restore_files(
path: String,
files: Vec<String>,
staged: bool,
) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
validate_files(&files)?;
@@ -1481,6 +1593,86 @@ pub async fn rebase_branch(path: String, branch: String) -> Result<GitStatus, St
.map_err(|err| format!("Could not rebase: {err}"))?
}
#[tauri::command]
pub fn list_interactive_rebase_commits(
path: String,
base: String,
) -> Result<Vec<RebaseCommit>, String> {
let repo = resolve_repo(&path)?;
interactive_rebase_commits_for_repo(&repo, &base)
}
#[tauri::command]
pub async fn start_interactive_rebase(
path: String,
base: String,
plan: Vec<RebasePlanItem>,
) -> Result<GitStatus, String> {
tauri::async_runtime::spawn_blocking(move || -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let status = status_for_repo(&repo)?;
if !status.clean {
return Err(
"Commit or stash working tree changes before starting an interactive rebase."
.to_string(),
);
}
if status.rebase_in_progress || status.cherry_pick_in_progress {
return Err(
"Finish the current Git operation before starting an interactive rebase."
.to_string(),
);
}
let base_hash = verify_commit(&repo, &base)?;
let available = interactive_rebase_commits_for_repo(&repo, &base_hash)?;
validate_rebase_plan(&available, &plan)?;
let todo = build_rebase_todo(&available, &plan)?;
let reword_queue = build_reword_queue(&available, &plan)?;
let git_dir = git_dir_for_repo(&repo)?;
cleanup_interactive_rebase_helpers(&repo);
let todo_path = git_dir.join(REBASE_TODO_FILE);
let reword_queue_path = git_dir.join(REWORD_QUEUE_FILE);
fs::write(&todo_path, todo)
.map_err(|err| format!("Could not prepare interactive rebase plan: {err}"))?;
fs::write(&reword_queue_path, reword_queue)
.map_err(|err| format!("Could not prepare reword messages: {err}"))?;
let sequence_helper_path = repo.join(sequence_helper_name());
let commit_helper_path = repo.join(commit_helper_name());
let current_exe = env::current_exe()
.map_err(|err| format!("Could not locate the Gitty executable: {err}"))?;
fs::copy(&current_exe, &sequence_helper_path)
.and_then(|_| fs::copy(&current_exe, &commit_helper_path))
.map_err(|err| format!("Could not prepare interactive rebase helpers: {err}"))?;
let sequence_editor_command = format!("./{}", sequence_helper_name());
let commit_editor_command = format!("./{}", commit_helper_name());
let output = git_command()
.arg("-C")
.arg(&repo)
.args(["rebase", "-i", base_hash.as_str()])
.env("GIT_SEQUENCE_EDITOR", sequence_editor_command)
.env(SEQUENCE_EDITOR_PLAN_ENV, &todo_path)
.env("GIT_EDITOR", commit_editor_command)
.env(COMMIT_EDITOR_QUEUE_ENV, &reword_queue_path)
.output()
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"));
let result = rebase_status_or_error(&repo, output?, "Interactive rebase failed", true);
let _ = fs::remove_file(&todo_path);
let _ = fs::remove_file(&sequence_helper_path);
if !matches!(&result, Ok(status) if status.rebase_in_progress) {
let _ = fs::remove_file(&reword_queue_path);
let _ = fs::remove_file(&commit_helper_path);
}
result
})
.await
.map_err(|err| format!("Could not run interactive rebase: {err}"))?
}
#[tauri::command]
pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
@@ -1488,15 +1680,27 @@ pub fn rebase_continue(path: String) -> Result<GitStatus, String> {
return Err("No rebase is currently in progress.".to_string());
}
let output = git_command()
.arg("-C")
.arg(&repo)
.args(["rebase", "--continue"])
.env("GIT_EDITOR", "true")
let git_dir = git_dir_for_repo(&repo)?;
let queue_path = git_dir.join(REWORD_QUEUE_FILE);
let helper_path = repo.join(commit_helper_name());
let mut command = git_command();
command.arg("-C").arg(&repo).args(["rebase", "--continue"]);
if queue_path.exists() && helper_path.exists() {
command
.env("GIT_EDITOR", format!("./{}", commit_helper_name()))
.env(COMMIT_EDITOR_QUEUE_ENV, &queue_path);
} else {
command.env("GIT_EDITOR", "true");
}
let output = command
.output()
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
rebase_status_or_error(&repo, output, "Rebase continue failed", false)
let result = rebase_status_or_error(&repo, output, "Rebase continue failed", false);
if !matches!(&result, Ok(status) if status.rebase_in_progress) {
cleanup_interactive_rebase_helpers(&repo);
}
result
}
#[tauri::command]
@@ -1507,9 +1711,249 @@ pub fn rebase_abort(path: String) -> Result<GitStatus, String> {
}
run_git(&repo, ["rebase", "--abort"])?;
cleanup_interactive_rebase_helpers(&repo);
status_for_repo(&repo)
}
#[tauri::command]
pub fn list_reflog(path: String, limit: Option<u32>) -> Result<Vec<ReflogEntry>, String> {
let repo = resolve_repo(&path)?;
let limit = limit.unwrap_or(250).clamp(1, 1_000).to_string();
let output = run_git(
&repo,
[
"reflog",
"show",
"--date=iso-strict",
"--format=%H%x1f%h%x1f%gD%x1f%gs%x1f%an%x1f%aI%x1e",
"-n",
limit.as_str(),
],
)?;
parse_reflog(&output)
}
#[tauri::command]
pub fn restore_reflog_entry(
path: String,
commit: String,
branch: String,
) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let status = status_for_repo(&repo)?;
if !status.clean {
return Err(
"Commit or stash working tree changes before restoring from the reflog.".to_string(),
);
}
let commit_hash = verify_commit(&repo, &commit)?;
let branch = validate_new_branch_name(&repo, &branch)?;
run_git(
&repo,
["checkout", "-b", branch.as_str(), commit_hash.as_str()],
)?;
status_for_repo(&repo)
}
fn interactive_rebase_commits_for_repo(
repo: &Path,
base: &str,
) -> Result<Vec<RebaseCommit>, String> {
let base_hash = verify_commit(repo, base)?;
let range = format!("{base_hash}..HEAD");
let merges = run_git(repo, ["rev-list", "--merges", range.as_str()])?;
if !String::from_utf8_lossy(&merges).trim().is_empty() {
return Err("Interactive rebase currently supports linear commit ranges only. Choose a base after the last merge commit.".to_string());
}
let output = run_git(
repo,
[
"log",
"--reverse",
"--format=%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1e",
range.as_str(),
],
)?;
let mut commits = Vec::new();
for raw in output.split(|byte| *byte == 0x1e) {
let record = String::from_utf8_lossy(raw);
let record = record.trim_matches(['\r', '\n', ' ']);
if record.is_empty() {
continue;
}
let fields = record.split('\x1f').collect::<Vec<_>>();
if fields.len() != 5 {
return Err("Git returned an unexpected interactive rebase record.".to_string());
}
commits.push(RebaseCommit {
hash: fields[0].to_string(),
short_hash: fields[1].to_string(),
summary: fields[2].to_string(),
author_name: fields[3].to_string(),
date: fields[4].to_string(),
});
}
Ok(commits)
}
fn validate_rebase_plan(commits: &[RebaseCommit], plan: &[RebasePlanItem]) -> Result<(), String> {
if commits.is_empty() {
return Err("There are no commits to rebase onto the selected base.".to_string());
}
if commits.len() != plan.len() {
return Err("The rebase plan must include every commit exactly once.".to_string());
}
let expected = commits
.iter()
.map(|item| item.hash.as_str())
.collect::<BTreeSet<_>>();
let actual = plan
.iter()
.map(|item| item.hash.as_str())
.collect::<BTreeSet<_>>();
if actual.len() != plan.len() || actual != expected {
return Err(
"The rebase plan contains missing, duplicate, or unexpected commits.".to_string(),
);
}
let mut has_kept_commit = false;
for item in plan {
match item.action {
RebaseAction::Drop => {}
RebaseAction::Squash | RebaseAction::Fixup if !has_kept_commit => {
return Err("Squash and fixup need an earlier picked commit.".to_string());
}
RebaseAction::Reword => {
let message = item.message.as_deref().unwrap_or("").trim();
if message.is_empty() || message.contains(['\r', '\n']) {
return Err("Reword messages must be a single non-empty line.".to_string());
}
has_kept_commit = true;
}
_ => has_kept_commit = true,
}
}
if !has_kept_commit {
return Err("Keep at least one commit in the interactive rebase plan.".to_string());
}
Ok(())
}
fn build_rebase_todo(commits: &[RebaseCommit], plan: &[RebasePlanItem]) -> Result<String, String> {
let by_hash = commits
.iter()
.map(|commit| (commit.hash.as_str(), commit))
.collect::<BTreeMap<_, _>>();
let mut todo = String::new();
for item in plan {
let commit = by_hash
.get(item.hash.as_str())
.ok_or_else(|| "The rebase plan references an unknown commit.".to_string())?;
let summary = commit.summary.replace(['\r', '\n'], " ");
match item.action {
RebaseAction::Pick => todo.push_str(&format!("pick {} {}\n", item.hash, summary)),
RebaseAction::Reword => todo.push_str(&format!("reword {} {}\n", item.hash, summary)),
RebaseAction::Squash => todo.push_str(&format!("squash {} {}\n", item.hash, summary)),
RebaseAction::Fixup => todo.push_str(&format!("fixup {} {}\n", item.hash, summary)),
RebaseAction::Drop => todo.push_str(&format!("drop {} {}\n", item.hash, summary)),
}
}
Ok(todo)
}
fn build_reword_queue(commits: &[RebaseCommit], plan: &[RebasePlanItem]) -> Result<String, String> {
let by_hash = commits
.iter()
.map(|commit| (commit.hash.as_str(), commit))
.collect::<BTreeMap<_, _>>();
let mut queue = String::new();
for item in plan
.iter()
.filter(|item| item.action == RebaseAction::Reword)
{
let commit = by_hash
.get(item.hash.as_str())
.ok_or_else(|| "The rebase plan references an unknown commit.".to_string())?;
let message = item.message.as_deref().unwrap_or("").trim();
queue.push_str(&commit.summary.replace(['\r', '\n'], " "));
queue.push('\x1f');
queue.push_str(message);
queue.push('\x1e');
}
Ok(queue)
}
fn git_dir_for_repo(repo: &Path) -> Result<PathBuf, String> {
let output = run_git(repo, ["rev-parse", "--absolute-git-dir"])?;
let path = String::from_utf8_lossy(&output).trim().to_string();
if path.is_empty() {
Err("Git could not determine its metadata directory.".to_string())
} else {
Ok(PathBuf::from(path))
}
}
fn sequence_helper_name() -> &'static str {
if cfg!(windows) {
".gitty-sequence-editor.exe"
} else {
SEQUENCE_HELPER_STEM
}
}
fn commit_helper_name() -> &'static str {
if cfg!(windows) {
".gitty-commit-editor.exe"
} else {
COMMIT_HELPER_STEM
}
}
fn cleanup_interactive_rebase_helpers(repo: &Path) {
if let Ok(git_dir) = git_dir_for_repo(repo) {
let _ = fs::remove_file(git_dir.join(REBASE_TODO_FILE));
let _ = fs::remove_file(git_dir.join(REWORD_QUEUE_FILE));
}
let _ = fs::remove_file(repo.join(sequence_helper_name()));
let _ = fs::remove_file(repo.join(commit_helper_name()));
}
fn is_interactive_rebase_helper_path(path: &str) -> bool {
matches!(
path.replace('\\', "/").rsplit('/').next(),
Some(name) if name == sequence_helper_name() || name == commit_helper_name()
)
}
fn parse_reflog(output: &[u8]) -> Result<Vec<ReflogEntry>, String> {
let mut entries = Vec::new();
for raw in output.split(|byte| *byte == 0x1e) {
let record = String::from_utf8_lossy(raw);
let record = record.trim_matches(['\r', '\n', ' ']);
if record.is_empty() {
continue;
}
let fields = record.split('\x1f').collect::<Vec<_>>();
if fields.len() != 6 {
return Err("Git returned an unexpected reflog record.".to_string());
}
entries.push(ReflogEntry {
hash: fields[0].to_string(),
short_hash: fields[1].to_string(),
selector: fields[2].to_string(),
action: fields[3].to_string(),
author_name: fields[4].to_string(),
date: fields[5].to_string(),
});
}
Ok(entries)
}
fn rebase_status_or_error(
repo: &Path,
output: Output,
@@ -2521,6 +2965,7 @@ fn status_for_repo(repo: &Path) -> Result<GitStatus, String> {
)?;
let (branch, mut files) = parse_status_output(&output)?;
detect_worktree_renames(repo, &mut files);
files.retain(|file| !is_interactive_rebase_helper_path(&file.path));
Ok(GitStatus {
repo_path: repo.to_string_lossy().to_string(),
@@ -4582,6 +5027,134 @@ mod tests {
assert_eq!(branch.behind, 0);
}
#[test]
fn parses_reflog_records() {
let raw = b"1111111111111111111111111111111111111111\x1f1111111\x1fHEAD@{0}\x1fcommit: Add feature\x1fAda\x1f2026-07-10T12:00:00+02:00\x1e";
let entries = parse_reflog(raw).expect("reflog should parse");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].selector, "HEAD@{0}");
assert_eq!(entries[0].action, "commit: Add feature");
}
#[test]
fn commit_editor_applies_only_the_matching_reword_message() {
let temp = temp_dir("commit_editor");
let queue = temp.path.join("queue");
let message = temp.path.join("COMMIT_EDITMSG");
fs::write(
&queue,
"first commit\x1frenamed first\x1esecond commit\x1frenamed second\x1e",
)
.expect("queue should be written");
fs::write(
&message,
"first commit\n\n# Please enter the commit message\n",
)
.expect("message should be written");
apply_reword_message(&queue, &message).expect("message should be applied");
assert_eq!(fs::read_to_string(&message).unwrap(), "renamed first\n");
assert_eq!(
fs::read_to_string(&queue).unwrap(),
"second commit\x1frenamed second\x1e"
);
}
#[test]
fn interactive_rebase_builds_a_valid_reword_and_squash_plan() {
let repo = init_temp_repo("interactive_rebase");
commit_initial_file(&repo.path);
let base = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
fs::write(repo.path.join("first.txt"), "first\n").expect("first file should be written");
run_git_test(&repo.path, ["add", "first.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "first commit"]);
fs::write(repo.path.join("second.txt"), "second\n").expect("second file should be written");
run_git_test(&repo.path, ["add", "second.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "second commit"]);
let commits = interactive_rebase_commits_for_repo(&repo.path, &base)
.expect("rebase commits should load");
assert_eq!(commits.len(), 2);
let plan = vec![
RebasePlanItem {
hash: commits[0].hash.clone(),
action: RebaseAction::Reword,
message: Some("combined feature".to_string()),
},
RebasePlanItem {
hash: commits[1].hash.clone(),
action: RebaseAction::Squash,
message: None,
},
];
validate_rebase_plan(&commits, &plan).expect("plan should be valid");
let todo = build_rebase_todo(&commits, &plan).expect("todo should build");
assert!(todo.contains(&format!("reword {} first commit", commits[0].hash)));
assert!(todo.contains(&format!("squash {} second commit", commits[1].hash)));
assert_eq!(
build_reword_queue(&commits, &plan).expect("queue should build"),
"first commit\x1fcombined feature\x1e"
);
}
#[test]
fn interactive_rebase_accepts_a_diverged_base_branch() {
let repo = init_temp_repo("interactive_rebase_diverged");
commit_initial_file(&repo.path);
let main_branch = git_output_test(&repo.path, ["branch", "--show-current"]);
run_git_test(&repo.path, ["checkout", "-q", "-b", "feature"]);
fs::write(repo.path.join("feature.txt"), "feature\n")
.expect("feature file should be written");
run_git_test(&repo.path, ["add", "feature.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "feature commit"]);
run_git_test(&repo.path, ["checkout", "-q", main_branch.as_str()]);
fs::write(repo.path.join("main.txt"), "main\n").expect("main file should be written");
run_git_test(&repo.path, ["add", "main.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "main advanced"]);
run_git_test(&repo.path, ["checkout", "-q", "feature"]);
let commits = interactive_rebase_commits_for_repo(&repo.path, &main_branch)
.expect("diverged base should be accepted");
assert_eq!(commits.len(), 1);
assert_eq!(commits[0].summary, "feature commit");
}
#[test]
fn reflog_restore_creates_a_recovery_branch_without_resetting_existing_branch() {
let repo = init_temp_repo("reflog_restore");
commit_initial_file(&repo.path);
let initial = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
let original_branch = git_output_test(&repo.path, ["branch", "--show-current"]);
fs::write(repo.path.join("later.txt"), "later\n").expect("later file should be written");
run_git_test(&repo.path, ["add", "later.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "later"]);
let entries = list_reflog(repo.path.to_string_lossy().to_string(), Some(20))
.expect("reflog should load");
assert!(entries.len() >= 2);
let status = restore_reflog_entry(
repo.path.to_string_lossy().to_string(),
initial.clone(),
"recovery/initial".to_string(),
)
.expect("recovery branch should be created");
assert_eq!(status.current_branch.as_deref(), Some("recovery/initial"));
assert_eq!(git_output_test(&repo.path, ["rev-parse", "HEAD"]), initial);
assert!(
ref_exists(&repo.path, &format!("refs/heads/{original_branch}"))
.expect("original branch should still exist")
);
}
#[test]
fn parses_commit_history_records() {
let raw = b"1111111111111111111111111111111111111111\x1f1111111\x1fAda Lovelace\x1fada@example.com\x1f2026-06-26T12:34:56+02:00\x1fHEAD -> main, tag: v1\x1f2222222222222222222222222222222222222222 3333333333333333333333333333333333333333\x1fAdd history panel\x1e";
+23 -8
View File
@@ -12,14 +12,16 @@ use git::{
compare_file_to_parent, create_branch, create_tag, cred_delete, cred_load, cred_save,
delete_branch, delete_tag, diff_file_against_working_tree, fetch, get_file_blame,
get_file_patch, get_remote_url, get_status, last_commit_message, list_branches, list_commits,
list_file_history, list_repository_files, list_stashes, list_tags, merge_branch,
open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull,
push, push_tag, read_conflict, rebase_abort, rebase_branch, rebase_continue, rename_branch,
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop, stash_pop,
stash_push, undo_last_commit, unstage_files,
list_file_history, list_interactive_rebase_commits, list_reflog, list_repository_files,
list_stashes, list_tags, merge_branch, open_repo_in_explorer, open_repository,
open_repository_bundle, open_repository_file, pull, push, push_tag, read_conflict,
rebase_abort, rebase_branch, rebase_continue, rename_branch, resolve_conflict,
resolve_conflict_side, restore_file_from_commit, restore_files, restore_reflog_entry,
restore_to_commit, run_sequence_editor_if_requested, search_code_introductions, stage_files,
start_interactive_rebase, stash_apply, stash_drop, stash_pop, stash_push, undo_last_commit,
unstage_files,
};
use tauri::{Manager, AppHandle};
use tauri::Manager;
#[tauri::command]
fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
@@ -42,10 +44,19 @@ fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
#[tokio::main]
async fn main() {
if let Some(result) = run_sequence_editor_if_requested() {
if let Err(error) = result {
eprintln!("{error}");
std::process::exit(1);
}
return;
}
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
#[cfg(desktop)]
let _ = app.get_webview_window("main")
let _ = app
.get_webview_window("main")
.expect("no main window")
.set_focus();
}))
@@ -107,6 +118,10 @@ async fn main() {
rebase_branch,
rebase_continue,
rebase_abort,
list_interactive_rebase_commits,
start_interactive_rebase,
list_reflog,
restore_reflog_entry,
list_repository_files,
open_repository_bundle,
list_file_history,