From 19e41379901a84d0b59f373621b7c696244381b2 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 18 Sep 2025 00:45:53 +0800 Subject: [PATCH 1/7] refactor finbr add dirty checking --- src/dir_marks.rs | 8 +-- src/git.rs | 134 ++++++++++++++++++++++++++++++++++++--------- src/main.rs | 14 ++--- src/tags.rs | 139 ++++++++++++++++++++++------------------------- 4 files changed, 185 insertions(+), 110 deletions(-) diff --git a/src/dir_marks.rs b/src/dir_marks.rs index ffa99f9..07bda00 100644 --- a/src/dir_marks.rs +++ b/src/dir_marks.rs @@ -82,7 +82,7 @@ impl DirMarks { .marks .push(Bookmark::new(kwd.to_owned(), path.to_owned(), description.to_owned())); } - println!("added `{}`\t{}\t{}", kwd, path, description) + println!("added `{kwd}`\t{path}\t{description}") } pub fn remove(&mut self, kwd: &str) { @@ -91,7 +91,7 @@ impl DirMarks { println!("removed `{}`\t{}\t{}", target.kwd, target.path, target.description); self.marks.retain(|m| m.kwd != kwd); } else { - println!("`{}` not found", kwd); + println!("`{kwd}` not found"); } } @@ -103,7 +103,7 @@ impl DirMarks { pub fn jump(&mut self, kwd: &str) -> Result<(), String> { // remove file in `JUMP_TARGET_DATA_PATH`` first if std::path::Path::new(JUMP_TARGET_DATA_PATH).exists() { - std::fs::remove_file(JUMP_TARGET_DATA_PATH).map_err(|e| format!("failed to remove {}", e))?; + std::fs::remove_file(JUMP_TARGET_DATA_PATH).map_err(|e| format!("failed to remove {e}"))?; } // match by exact keyword @@ -218,6 +218,6 @@ impl DirMarks { /// print shell function for zsh pub fn shell_fn() { - println!("{}", SHELL_FN_GG); + println!("{SHELL_FN_GG}"); } } diff --git a/src/git.rs b/src/git.rs index 6fbc5a5..878c328 100644 --- a/src/git.rs +++ b/src/git.rs @@ -3,11 +3,14 @@ use std::io::{self, Write}; use std::process::Command; /// detect main or master branch first, get current branch, +/// check for dirty state and handle stashing if needed, /// then fetch origin/main --prune, switch to main, pull latest changes -/// finally delete that branch +/// finally delete that branch and restore stashed changes if any pub fn finish_branch() -> Result<(), Box> { let repo = Repository::open(".")?; + println!("๐Ÿ” Starting branch finish process..."); + // Get the current branch let head = repo.head()?; let current_branch_name = if let Some(name) = head.shorthand() { @@ -16,39 +19,42 @@ pub fn finish_branch() -> Result<(), Box> { return Err("Not on a branch".into()); }; + println!("๐Ÿ“ Current branch: {current_branch_name}"); + // Check if we're already on main or master if current_branch_name == "main" || current_branch_name == "master" { - println!("Already on {} branch, nothing to do", current_branch_name); + println!("โœ… Already on {current_branch_name} branch, nothing to do"); return Ok(()); } + // Check for dirty working directory + let has_stashed_changes = check_and_handle_dirty_state()?; + // Detect the main branch (main or master) let main_branch = detect_main_branch(&repo)?; - println!("Detected main branch: {}", main_branch); + println!("๐ŸŽฏ Detected main branch: {main_branch}"); // Switch to main branch - println!("Switching to {} branch...", main_branch); + println!("๐Ÿ”„ Switching to {main_branch} branch..."); checkout_branch(&repo, &main_branch)?; + println!("โœ… Successfully switched to {main_branch} branch"); // Fetch and pull latest changes on main branch in one operation - println!("Fetching and pulling latest changes on {} branch...", main_branch); + println!("๐Ÿ“ฅ Fetching and pulling latest changes on {main_branch} branch..."); fetch_and_pull(&repo, &main_branch)?; - // Delete the feature branch - print!("Delete branch '{}'? (y/N): ", current_branch_name); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; + // Delete the feature branch (no confirmation needed) + println!("๐Ÿ—‘๏ธ Deleting branch '{current_branch_name}'..."); + delete_branch(&repo, ¤t_branch_name)?; + println!("โœ… Successfully deleted branch '{current_branch_name}'"); - if input.trim().to_lowercase() == "y" || input.trim().to_lowercase() == "yes" { - delete_branch(&repo, ¤t_branch_name)?; - println!("Deleted branch '{}'", current_branch_name); - } else { - println!("Branch '{}' was not deleted", current_branch_name); + // Restore stashed changes if any + if has_stashed_changes { + println!("๐Ÿ”„ Restoring previously stashed changes..."); + restore_stashed_changes()?; } - println!("Finished! You are now on the {} branch", main_branch); + println!("๐ŸŽ‰ Finished! You are now on the {main_branch} branch"); Ok(()) } @@ -80,22 +86,20 @@ fn detect_main_branch(repo: &Repository) -> Result Result<(), Box> { - // Check if there are uncommitted changes first - let status = Command::new("git").args(["status", "--porcelain"]).output()?; - if !status.stdout.is_empty() { - return Err("There are uncommitted changes. Please commit or stash them first.".into()); - } - // Fetch all branches with prune and then merge the remote tracking branch // This is more efficient than separate fetch + pull commands + println!("๐Ÿ“ก Fetching from origin with --prune..."); let status = Command::new("git").args(["fetch", "origin", "--prune"]).status()?; if !status.success() { return Err(format!("Failed to fetch from origin with exit code: {}", status.code().unwrap_or(-1)).into()); } + println!("โœ… Successfully fetched from origin"); + // Now merge the remote tracking branch (equivalent to pull but without redundant fetch) - let status = Command::new("git").args(["merge", &format!("origin/{}", main_branch)]).status()?; + println!("๐Ÿ”„ Merging origin/{main_branch}..."); + let status = Command::new("git").args(["merge", &format!("origin/{main_branch}")]).status()?; if !status.success() { return Err( @@ -108,7 +112,7 @@ fn fetch_and_pull(_repo: &Repository, main_branch: &str) -> Result<(), Box Result<(), Box Result<(), Box Result> { + // Check if there are uncommitted changes + let status = Command::new("git").args(["status", "--porcelain"]).output()?; + + if status.stdout.is_empty() { + println!("โœ… Working directory is clean"); + return Ok(false); + } + + // Show what changes would be stashed + println!("โš ๏ธ Detected uncommitted changes in working directory:"); + let status_output = String::from_utf8_lossy(&status.stdout); + for line in status_output.lines() { + println!(" {line}"); + } + + // Ask user for confirmation + print!("๐Ÿ“ฆ Do you want to stash these changes before proceeding? (y/N): "); + io::stdout().flush()?; + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + + if input.trim().to_lowercase() == "y" || input.trim().to_lowercase() == "yes" { + println!("๐Ÿ“ฆ Stashing uncommitted changes..."); + + // Create a stash with a descriptive message + let stash_message = format!( + "Auto-stash before branch finish at {}", + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs() + ); + + let status = Command::new("git").args(["stash", "push", "-m", &stash_message]).status()?; + + if !status.success() { + return Err("Failed to stash changes".into()); + } + + println!("โœ… Successfully stashed changes with message: '{stash_message}'"); + Ok(true) + } else { + Err("Cannot proceed with uncommitted changes. Please commit or stash them manually.".into()) + } +} + +fn restore_stashed_changes() -> Result<(), Box> { + // Apply the most recent stash + let status = Command::new("git").args(["stash", "pop"]).output()?; + + if !status.status.success() { + let error_msg = String::from_utf8_lossy(&status.stderr); + if error_msg.contains("No stash entries found") { + println!("โ„น๏ธ No stash entries to restore"); + return Ok(()); + } else { + println!("โš ๏ธ Warning: Failed to automatically restore stashed changes:"); + println!(" {}", error_msg.trim()); + println!(" You can manually restore them later with: git stash pop"); + return Ok(()); // Don't fail the entire operation + } + } + + println!("โœ… Successfully restored stashed changes"); + + // Show what was restored + let restored_output = String::from_utf8_lossy(&status.stdout); + if !restored_output.trim().is_empty() { + println!("๐Ÿ“‹ Restored changes:"); + for line in restored_output.lines() { + if !line.trim().is_empty() { + println!(" {line}"); + } + } + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index fbb6a77..99d0efb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,12 +22,12 @@ fn main() -> Result<(), String> { let network_interfaces = list_afinet_netifas().unwrap(); for (name, ip) in network_interfaces.iter() { - println!("{}:\t{:?}", name, ip); + println!("{name}:\t{ip:?}"); } } else { let my_local_ip = local_ip().unwrap(); cli_clipboard::set_contents(my_local_ip.to_string()).expect("write to clipboard"); - println!("{}\t\t(copied to clipboard)", my_local_ip); + println!("{my_local_ip}\t\t(copied to clipboard)"); } } CopyFile(options) => { @@ -62,10 +62,10 @@ fn main() -> Result<(), String> { for (pid, process) in sys.processes() { println!("{}\t#{pid}", process.name().to_string_lossy()); if let Some(v) = process.cwd() { - print!("\t{:?}", v); + print!("\t{v:?}"); } if let Some(v) = process.user_id() { - print!("\t{:?}", v); + print!("\t{v:?}"); } println!(); // println!(" {}", process.cmd().join(" ")); @@ -79,7 +79,7 @@ fn main() -> Result<(), String> { }; let dir_str = dir.display().to_string(); cli_clipboard::set_contents(dir_str.to_owned()).expect("write to clipboard"); - println!("{}\t\t(copied to clipboard)", dir_str); + println!("{dir_str}\t\t(copied to clipboard)"); } ListFileSize(options) => { show_file_size::show_file_size(options)?; @@ -120,13 +120,13 @@ fn main() -> Result<(), String> { }, FinishBranch(_) => { if let Err(e) = git::finish_branch() { - eprintln!("Error finishing branch: {}", e); + eprintln!("Error finishing branch: {e}"); std::process::exit(1); } } ShowTags(options) => { if let Err(e) = tags::show_tags(&options) { - eprintln!("Error showing tags: {}", e); + eprintln!("Error showing tags: {e}"); std::process::exit(1); } } diff --git a/src/tags.rs b/src/tags.rs index d864cf0..5c09dea 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -3,91 +3,84 @@ use std::process::Command; #[derive(Clone)] struct TagInfo { - date: String, - name: String, - subject: String, + date: String, + name: String, + subject: String, } pub fn show_tags(options: &InspectForTags) -> Result<(), String> { - let output = Command::new("git") - .args([ - "for-each-ref", - "--sort=creatordate", // Sort by date ascending - "--format=%(creatordate:short)\t%(refname:short)\t%(subject)", - "refs/tags", - ]) - .output() - .map_err(|e| e.to_string())?; + let output = Command::new("git") + .args([ + "for-each-ref", + "--sort=creatordate", // Sort by date ascending + "--format=%(creatordate:short)\t%(refname:short)\t%(subject)", + "refs/tags", + ]) + .output() + .map_err(|e| e.to_string())?; - if !output.status.success() { - return Err(String::from_utf8_lossy(&output.stderr).to_string()); - } + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).to_string()); + } - let output_str = String::from_utf8_lossy(&output.stdout); - if output_str.is_empty() { - println!("No tags found."); - return Ok(()); - } + let output_str = String::from_utf8_lossy(&output.stdout); + if output_str.is_empty() { + println!("No tags found."); + return Ok(()); + } - let all_tags: Vec = output_str - .lines() - .filter_map(|line| { - let parts: Vec<&str> = line.splitn(3, '\t').collect(); - if parts.len() == 3 { - Some(TagInfo { - date: parts[0].to_string(), - name: parts[1].to_string(), - subject: parts[2].to_string(), - }) - } else { - None - } + let all_tags: Vec = output_str + .lines() + .filter_map(|line| { + let parts: Vec<&str> = line.splitn(3, '\t').collect(); + if parts.len() == 3 { + Some(TagInfo { + date: parts[0].to_string(), + name: parts[1].to_string(), + subject: parts[2].to_string(), }) - .collect(); + } else { + None + } + }) + .collect(); - let total_tags = all_tags.len(); - let tags_to_display = if options.all { - all_tags.clone() - } else { - let tags: Vec = all_tags.iter().rev().take(10).cloned().collect(); - tags.into_iter().rev().collect() - }; + let total_tags = all_tags.len(); + let tags_to_display = if options.all { + all_tags.clone() + } else { + let tags: Vec = all_tags.iter().rev().take(10).cloned().collect(); + tags.into_iter().rev().collect() + }; - if tags_to_display.is_empty() { - println!("No tags to display."); - return Ok(()); - } + if tags_to_display.is_empty() { + println!("No tags to display."); + return Ok(()); + } - if !options.all && total_tags > tags_to_display.len() { - if let (Some(first_tag), Some(last_tag)) = (all_tags.first(), all_tags.last()) { - println!( - "Showing the last {} of {} tags (from {} to {}). Use --all to see all.", - tags_to_display.len(), - total_tags, - first_tag.date, - last_tag.date - ); - println!(); // Add a blank line for separation - } + if !options.all && total_tags > tags_to_display.len() { + if let (Some(first_tag), Some(last_tag)) = (all_tags.first(), all_tags.last()) { + println!( + "Showing the last {} of {} tags (from {} to {}). Use --all to see all.", + tags_to_display.len(), + total_tags, + first_tag.date, + last_tag.date + ); + println!(); // Add a blank line for separation } + } - - let mut max_tag_width = 0; - for tag in &tags_to_display { - if tag.name.len() > max_tag_width { - max_tag_width = tag.name.len(); - } + let mut max_tag_width = 0; + for tag in &tags_to_display { + if tag.name.len() > max_tag_width { + max_tag_width = tag.name.len(); } + } - for tag in tags_to_display { - println!( - "{} {: Date: Thu, 18 Sep 2025 00:53:10 +0800 Subject: [PATCH 2/7] check merged status before finish --- src/git.rs | 107 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 34 deletions(-) diff --git a/src/git.rs b/src/git.rs index 878c328..df2f6a1 100644 --- a/src/git.rs +++ b/src/git.rs @@ -39,9 +39,24 @@ pub fn finish_branch() -> Result<(), Box> { checkout_branch(&repo, &main_branch)?; println!("โœ… Successfully switched to {main_branch} branch"); - // Fetch and pull latest changes on main branch in one operation - println!("๐Ÿ“ฅ Fetching and pulling latest changes on {main_branch} branch..."); - fetch_and_pull(&repo, &main_branch)?; + // First fetch to get the latest remote history + println!("๐Ÿ“ก Fetching latest changes from remote..."); + fetch_remote()?; + + // Check if the current branch has been merged into main + println!("๐Ÿ” Checking if branch '{current_branch_name}' has been merged into '{main_branch}'..."); + if !is_branch_merged(¤t_branch_name, &main_branch)? { + println!("โš ๏ธ Warning: Branch '{current_branch_name}' has not been merged into '{main_branch}'!"); + println!(" This means the branch contains commits that are not in the main branch."); + println!(" Please merge or rebase your branch first before using this command."); + println!(" Aborting to prevent data loss."); + return Err("Branch not merged - cannot safely delete".into()); + } + println!("โœ… Branch '{current_branch_name}' has been merged into '{main_branch}'"); + + // Pull latest changes on main branch + println!("๐Ÿ“ฅ Pulling latest changes on {main_branch} branch..."); + pull_main_branch(&main_branch)?; // Delete the feature branch (no confirmation needed) println!("๐Ÿ—‘๏ธ Deleting branch '{current_branch_name}'..."); @@ -85,37 +100,6 @@ fn detect_main_branch(repo: &Repository) -> Result Result<(), Box> { - // Fetch all branches with prune and then merge the remote tracking branch - // This is more efficient than separate fetch + pull commands - println!("๐Ÿ“ก Fetching from origin with --prune..."); - let status = Command::new("git").args(["fetch", "origin", "--prune"]).status()?; - - if !status.success() { - return Err(format!("Failed to fetch from origin with exit code: {}", status.code().unwrap_or(-1)).into()); - } - - println!("โœ… Successfully fetched from origin"); - - // Now merge the remote tracking branch (equivalent to pull but without redundant fetch) - println!("๐Ÿ”„ Merging origin/{main_branch}..."); - let status = Command::new("git").args(["merge", &format!("origin/{main_branch}")]).status()?; - - if !status.success() { - return Err( - format!( - "Failed to merge origin/{} with exit code: {}", - main_branch, - status.code().unwrap_or(-1) - ) - .into(), - ); - } - - println!("โœ… Successfully fetched and updated {main_branch} branch"); - Ok(()) -} - fn checkout_branch(repo: &Repository, branch_name: &str) -> Result<(), Box> { let branch = repo.find_branch(branch_name, BranchType::Local)?; let branch_ref = branch.get(); @@ -210,3 +194,58 @@ fn restore_stashed_changes() -> Result<(), Box> { Ok(()) } + +fn fetch_remote() -> Result<(), Box> { + println!("๐Ÿ“ก Fetching from origin with --prune..."); + let status = Command::new("git").args(["fetch", "origin", "--prune"]).status()?; + + if !status.success() { + return Err(format!("Failed to fetch from origin with exit code: {}", status.code().unwrap_or(-1)).into()); + } + + println!("โœ… Successfully fetched from origin"); + Ok(()) +} + +fn is_branch_merged(branch_name: &str, main_branch: &str) -> Result> { + // Use git merge-base to check if the branch has been merged + // If the merge-base of the branch and main is the same as the branch's HEAD, + // then the branch has been fully merged into main + let branch_head = Command::new("git").args(["rev-parse", branch_name]).output()?; + + if !branch_head.status.success() { + return Err(format!("Failed to get HEAD of branch '{branch_name}'").into()); + } + + let branch_head_hash = String::from_utf8_lossy(&branch_head.stdout).trim().to_string(); + + let merge_base = Command::new("git").args(["merge-base", branch_name, main_branch]).output()?; + + if !merge_base.status.success() { + return Err(format!("Failed to find merge-base between '{branch_name}' and '{main_branch}'").into()); + } + + let merge_base_hash = String::from_utf8_lossy(&merge_base.stdout).trim().to_string(); + + // If the branch HEAD is the same as the merge-base, the branch is fully merged + Ok(branch_head_hash == merge_base_hash) +} + +fn pull_main_branch(main_branch: &str) -> Result<(), Box> { + println!("๐Ÿ”„ Merging origin/{main_branch}..."); + let status = Command::new("git").args(["merge", &format!("origin/{main_branch}")]).status()?; + + if !status.success() { + return Err( + format!( + "Failed to merge origin/{} with exit code: {}", + main_branch, + status.code().unwrap_or(-1) + ) + .into(), + ); + } + + println!("โœ… Successfully updated {main_branch} branch"); + Ok(()) +} From edc78eef7c12677a81a9ebbe1343bfc6d81aedbc Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 18 Sep 2025 00:54:57 +0800 Subject: [PATCH 3/7] refactor command to has br open --- src/args.rs | 27 ++++++++++++++++++--- src/git.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 18 ++++++++++---- 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/src/args.rs b/src/args.rs index 097087d..93af325 100644 --- a/src/args.rs +++ b/src/args.rs @@ -17,7 +17,7 @@ pub enum InspectionCommand { ShowWorkingDirectory(InspectForWorkingDirectory), ListFileSize(InspectForFileSize), DirMark(InspectForDirMark), - FinishBranch(InspectForFinishBranch), + Branch(InspectForBranch), ShowTags(InspectForTags), } @@ -93,11 +93,30 @@ pub struct InspectForDirMark { pub subcommand: DirMarkCommand, } -// InspectForFinishBranch +/// command for branch operations +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "br")] +pub struct InspectForBranch { + #[argh(subcommand)] + pub subcommand: BranchCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +pub enum BranchCommand { + Finish(InspectForBranchFinish), + Open(InspectForBranchOpen), +} + /// command for finishing a branch #[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "finbr")] -pub struct InspectForFinishBranch {} +#[argh(subcommand, name = "finish")] +pub struct InspectForBranchFinish {} + +/// command for opening remote repository +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "open")] +pub struct InspectForBranchOpen {} #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand)] diff --git a/src/git.rs b/src/git.rs index df2f6a1..852b935 100644 --- a/src/git.rs +++ b/src/git.rs @@ -73,6 +73,76 @@ pub fn finish_branch() -> Result<(), Box> { Ok(()) } +pub fn open_remote_repository() -> Result<(), String> { + use std::fs; + use std::process::Command; + + // ่ฏปๅ– .git/config ๆ–‡ไปถ + let config_content = fs::read_to_string(".git/config") + .map_err(|e| format!("Failed to read .git/config: {e}"))?; + + // ๆŸฅๆ‰พ remote "origin" ้ƒจๅˆ†็š„ url + let mut in_origin_section = false; + let mut remote_url = None; + + for line in config_content.lines() { + let line = line.trim(); + + if line == "[remote \"origin\"]" { + in_origin_section = true; + continue; + } + + if line.starts_with('[') && line != "[remote \"origin\"]" { + in_origin_section = false; + continue; + } + + if in_origin_section && line.starts_with("url = ") { + let url = line.strip_prefix("url = ").unwrap(); + remote_url = Some(url.to_string()); + break; + } + } + + let url = remote_url.ok_or("No remote origin URL found in .git/config")?; + + // ่ฝฌๆข Git URL ไธบ HTTP URL + let web_url = if url.starts_with("git@") { + // SSH format: git@github.com:user/repo.git -> https://github.com/user/repo + let without_git = url.strip_prefix("git@").unwrap(); + let parts: Vec<&str> = without_git.split(':').collect(); + if parts.len() == 2 { + let host = parts[0]; + let path = parts[1].strip_suffix(".git").unwrap_or(parts[1]); + format!("https://{host}/{path}") + } else { + return Err("Invalid SSH Git URL format".to_string()); + } + } else if url.starts_with("https://") { + // HTTPS format: already web-compatible, just remove .git suffix if present + url.strip_suffix(".git").unwrap_or(&url).to_string() + } else { + return Err("Unsupported Git URL format".to_string()); + }; + + println!("๐ŸŒ Opening remote repository: {web_url}"); + + // ไฝฟ็”จ็ณป็ปŸ้ป˜่ฎคๆต่งˆๅ™จๆ‰“ๅผ€ URL + let result = Command::new("open") + .arg(&web_url) + .output() + .map_err(|e| format!("Failed to open browser: {e}"))?; + + if !result.status.success() { + let stderr = String::from_utf8_lossy(&result.stderr); + return Err(format!("Failed to open URL: {stderr}")); + } + + println!("โœ… Successfully opened remote repository in browser"); + Ok(()) +} + fn detect_main_branch(repo: &Repository) -> Result> { // First try to find main branch if repo.find_branch("main", BranchType::Local).is_ok() { diff --git a/src/main.rs b/src/main.rs index 99d0efb..1c30cfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use sysinfo::System; use bytesize::ByteSize; -use args::{DirMarkCommand, InspectionCommand, TopLevelInspection}; +use args::{BranchCommand, DirMarkCommand, InspectionCommand, TopLevelInspection}; use local_ip_address::{list_afinet_netifas, local_ip}; fn main() -> Result<(), String> { @@ -118,10 +118,18 @@ fn main() -> Result<(), String> { dir_marks::DirMarks::shell_fn(); } }, - FinishBranch(_) => { - if let Err(e) = git::finish_branch() { - eprintln!("Error finishing branch: {e}"); - std::process::exit(1); + Branch(options) => match options.subcommand { + BranchCommand::Finish(_) => { + if let Err(e) = git::finish_branch() { + eprintln!("Error finishing branch: {e}"); + std::process::exit(1); + } + } + BranchCommand::Open(_) => { + if let Err(e) = git::open_remote_repository() { + eprintln!("Error opening remote repository: {e}"); + std::process::exit(1); + } } } ShowTags(options) => { From 3b6216f6a8d6e428ed5679e5b3f8741084b603da Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 18 Sep 2025 01:03:08 +0800 Subject: [PATCH 4/7] mention br subcommand in README --- README.md | 36 ++++++++++++++++++++++++++++++++++++ src/git.rs | 47 +++++++++++++++++++++++++++++++---------------- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 798aacc..cfe7298 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ in tags ``` Example output: + ``` Showing the last 10 of 12 tags (from 2025-07-31 to 2025-07-31). Use --all to see all. @@ -115,6 +116,41 @@ To view all tags, use the `--all` flag. in tags --all ``` +## Branch Operations + +The `in br` command provides Git branch management functionality. + +### Finish Branch + +Finish the current branch by merging it to the main branch and cleaning up: + +```bash +in br finish +``` + +This command will: + +- Check if the current branch is merged to the main branch +- Switch to the main branch +- Pull the latest changes +- Delete the feature branch +- Handle any uncommitted changes with stash if needed + +### Open Remote Repository + +Open the remote repository in your default browser: + +```bash +in br open +``` + +This command will: + +- Parse the `.git/config` file to find the remote origin URL +- Convert SSH/HTTPS Git URLs to web URLs +- For GitHub repositories, automatically navigate to the current branch +- Open the URL in your default browser + ### License MIT diff --git a/src/git.rs b/src/git.rs index 852b935..2f7490b 100644 --- a/src/git.rs +++ b/src/git.rs @@ -76,39 +76,49 @@ pub fn finish_branch() -> Result<(), Box> { pub fn open_remote_repository() -> Result<(), String> { use std::fs; use std::process::Command; - + + // ่Žทๅ–ๅฝ“ๅ‰ๅˆ†ๆ”ฏๅ + let repo = Repository::open(".").map_err(|e| format!("Failed to open repository: {e}"))?; + + let head = repo.head().map_err(|e| format!("Failed to get HEAD: {e}"))?; + + let current_branch = if let Some(branch_name) = head.shorthand() { + branch_name.to_string() + } else { + "HEAD".to_string() + }; + // ่ฏปๅ– .git/config ๆ–‡ไปถ - let config_content = fs::read_to_string(".git/config") - .map_err(|e| format!("Failed to read .git/config: {e}"))?; - + let config_content = fs::read_to_string(".git/config").map_err(|e| format!("Failed to read .git/config: {e}"))?; + // ๆŸฅๆ‰พ remote "origin" ้ƒจๅˆ†็š„ url let mut in_origin_section = false; let mut remote_url = None; - + for line in config_content.lines() { let line = line.trim(); - + if line == "[remote \"origin\"]" { in_origin_section = true; continue; } - + if line.starts_with('[') && line != "[remote \"origin\"]" { in_origin_section = false; continue; } - + if in_origin_section && line.starts_with("url = ") { let url = line.strip_prefix("url = ").unwrap(); remote_url = Some(url.to_string()); break; } } - + let url = remote_url.ok_or("No remote origin URL found in .git/config")?; - + // ่ฝฌๆข Git URL ไธบ HTTP URL - let web_url = if url.starts_with("git@") { + let mut web_url = if url.starts_with("git@") { // SSH format: git@github.com:user/repo.git -> https://github.com/user/repo let without_git = url.strip_prefix("git@").unwrap(); let parts: Vec<&str> = without_git.split(':').collect(); @@ -125,20 +135,25 @@ pub fn open_remote_repository() -> Result<(), String> { } else { return Err("Unsupported Git URL format".to_string()); }; - - println!("๐ŸŒ Opening remote repository: {web_url}"); - + + // ๅฆ‚ๆžœๆ˜ฏ GitHub ไป“ๅบ“๏ผŒๆทปๅŠ ๅˆ†ๆ”ฏไฟกๆฏ + if web_url.contains("github.com") && current_branch != "HEAD" { + web_url = format!("{web_url}/tree/{current_branch}"); + } + + println!("๐ŸŒ Opening remote repository: {web_url} (branch: {current_branch})"); + // ไฝฟ็”จ็ณป็ปŸ้ป˜่ฎคๆต่งˆๅ™จๆ‰“ๅผ€ URL let result = Command::new("open") .arg(&web_url) .output() .map_err(|e| format!("Failed to open browser: {e}"))?; - + if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); return Err(format!("Failed to open URL: {stderr}")); } - + println!("โœ… Successfully opened remote repository in browser"); Ok(()) } From 7c1602ffa6c8f4601f7340fdd7273707c9936843 Mon Sep 17 00:00:00 2001 From: tiye Date: Thu, 18 Sep 2025 14:30:49 +0800 Subject: [PATCH 5/7] get more strategies detecting merged status --- src/git.rs | 223 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 197 insertions(+), 26 deletions(-) diff --git a/src/git.rs b/src/git.rs index 2f7490b..fb31e67 100644 --- a/src/git.rs +++ b/src/git.rs @@ -34,39 +34,67 @@ pub fn finish_branch() -> Result<(), Box> { let main_branch = detect_main_branch(&repo)?; println!("๐ŸŽฏ Detected main branch: {main_branch}"); - // Switch to main branch - println!("๐Ÿ”„ Switching to {main_branch} branch..."); - checkout_branch(&repo, &main_branch)?; - println!("โœ… Successfully switched to {main_branch} branch"); - - // First fetch to get the latest remote history + // First fetch to get the latest remote history BEFORE checking merge status println!("๐Ÿ“ก Fetching latest changes from remote..."); - fetch_remote()?; + if let Err(e) = fetch_remote() { + println!("โš ๏ธ Warning: Failed to fetch from remote: {e}"); + println!(" Continuing with local state, but results may not be accurate"); + } - // Check if the current branch has been merged into main + // Check if the current branch has been merged into main BEFORE switching branches println!("๐Ÿ” Checking if branch '{current_branch_name}' has been merged into '{main_branch}'..."); - if !is_branch_merged(¤t_branch_name, &main_branch)? { + + let is_merged = match is_branch_merged(¤t_branch_name, &main_branch) { + Ok(merged) => merged, + Err(e) => { + println!("โŒ Failed to check merge status: {e}"); + println!(" Staying on current branch '{current_branch_name}' for safety"); + return Err("Failed to verify merge status - aborting to prevent data loss".into()); + } + }; + + if !is_merged { println!("โš ๏ธ Warning: Branch '{current_branch_name}' has not been merged into '{main_branch}'!"); println!(" This means the branch contains commits that are not in the main branch."); println!(" Please merge or rebase your branch first before using this command."); - println!(" Aborting to prevent data loss."); + println!(" Staying on current branch '{current_branch_name}' for safety"); return Err("Branch not merged - cannot safely delete".into()); } + println!("โœ… Branch '{current_branch_name}' has been merged into '{main_branch}'"); + // Now it's safe to switch to main branch + println!("๐Ÿ”„ Switching to {main_branch} branch..."); + if let Err(e) = checkout_branch(&repo, &main_branch) { + println!("โŒ Failed to switch to {main_branch} branch: {e}"); + println!(" Staying on current branch '{current_branch_name}' for safety"); + return Err(format!("Failed to switch to {main_branch} branch").into()); + } + println!("โœ… Successfully switched to {main_branch} branch"); + // Pull latest changes on main branch println!("๐Ÿ“ฅ Pulling latest changes on {main_branch} branch..."); - pull_main_branch(&main_branch)?; + if let Err(e) = pull_main_branch(&main_branch) { + println!("โš ๏ธ Warning: Failed to pull latest changes: {e}"); + println!(" Continuing with branch deletion..."); + } // Delete the feature branch (no confirmation needed) println!("๐Ÿ—‘๏ธ Deleting branch '{current_branch_name}'..."); - delete_branch(&repo, ¤t_branch_name)?; - println!("โœ… Successfully deleted branch '{current_branch_name}'"); + if let Err(e) = delete_branch(&repo, ¤t_branch_name) { + println!("โŒ Failed to delete branch '{current_branch_name}': {e}"); + println!(" You may need to delete it manually with: git branch -d {current_branch_name}"); + } else { + println!("โœ… Successfully deleted branch '{current_branch_name}'"); + } // Restore stashed changes if any if has_stashed_changes { println!("๐Ÿ”„ Restoring previously stashed changes..."); - restore_stashed_changes()?; + if let Err(e) = restore_stashed_changes() { + println!("โš ๏ธ Warning: Failed to restore stashed changes: {e}"); + println!(" You can manually restore them later with: git stash pop"); + } } println!("๐ŸŽ‰ Finished! You are now on the {main_branch} branch"); @@ -292,28 +320,171 @@ fn fetch_remote() -> Result<(), Box> { Ok(()) } -fn is_branch_merged(branch_name: &str, main_branch: &str) -> Result> { - // Use git merge-base to check if the branch has been merged - // If the merge-base of the branch and main is the same as the branch's HEAD, - // then the branch has been fully merged into main - let branch_head = Command::new("git").args(["rev-parse", branch_name]).output()?; +/// Helper function to execute git command with remote-first, local-fallback strategy +fn run_git_with_fallback(remote_args: &[&str], local_args: &[&str]) -> Result> { + let remote_result = Command::new("git").args(remote_args).output(); + + match remote_result { + Ok(output) if output.status.success() => Ok(output), + _ => { + let local_result = Command::new("git").args(local_args).output()?; + Ok(local_result) + } + } +} +/// Check if a branch appears in the merged branches list +fn check_branch_in_merged_list(branch_name: &str, main_branch: &str) -> Result> { + println!("๐Ÿ” Method 1: Checking with 'git branch --merged'..."); + + let output = run_git_with_fallback( + &["branch", "--merged", &format!("origin/{main_branch}")], + &["branch", "--merged", main_branch], + )?; + + if output.status.success() { + let merged_output = String::from_utf8_lossy(&output.stdout); + for line in merged_output.lines() { + let branch_line = line.trim().trim_start_matches("* ").trim(); + if branch_line == branch_name { + println!("โœ… Method 1: Branch found in merged branches list"); + return Ok(true); + } + } + } + + Ok(false) +} + +/// Check if all commits in branch exist in main branch +fn check_unmerged_commits(branch_name: &str, main_branch: &str) -> Result> { + println!("๐Ÿ” Method 2: Checking if all branch commits exist in main branch..."); + + let output = run_git_with_fallback( + &["rev-list", &format!("origin/{main_branch}..{branch_name}")], + &["rev-list", &format!("{main_branch}..{branch_name}")], + )?; + + if output.status.success() { + let unmerged_output_binding = String::from_utf8_lossy(&output.stdout); + let unmerged_output = unmerged_output_binding.trim(); + if unmerged_output.is_empty() { + println!("โœ… Method 2: No unmerged commits found - branch is fully merged"); + return Ok(true); + } + } + + Ok(false) +} + +/// Check remote tracking branches +fn check_remote_tracking_branches(branch_name: &str, main_branch: &str) -> Result> { + println!("๐Ÿ” Method 3: Checking remote tracking branches..."); + + let remote_branches = Command::new("git") + .args(["branch", "-r", "--merged", &format!("origin/{main_branch}")]) + .output()?; + + if remote_branches.status.success() { + let remote_output = String::from_utf8_lossy(&remote_branches.stdout); + let remote_branch_name = format!("origin/{branch_name}"); + + for line in remote_output.lines() { + let branch_line = line.trim(); + if branch_line == remote_branch_name { + println!("โœ… Method 3: Remote branch {remote_branch_name} found in merged list"); + return Ok(true); + } + } + } + + Ok(false) +} + +/// Check if branch is fast-forward merged using merge-base +fn check_fast_forward_merge(branch_name: &str, main_branch: &str) -> Result> { + println!("๐Ÿ” Method 4: Checking with merge-base (fast-forward detection)..."); + + let branch_head = Command::new("git").args(["rev-parse", branch_name]).output()?; if !branch_head.status.success() { - return Err(format!("Failed to get HEAD of branch '{branch_name}'").into()); + return Ok(false); } let branch_head_hash = String::from_utf8_lossy(&branch_head.stdout).trim().to_string(); - let merge_base = Command::new("git").args(["merge-base", branch_name, main_branch]).output()?; + let merge_base = run_git_with_fallback( + &["merge-base", branch_name, &format!("origin/{main_branch}")], + &["merge-base", branch_name, main_branch], + )?; + + if merge_base.status.success() { + let merge_base_hash = String::from_utf8_lossy(&merge_base.stdout).trim().to_string(); + if branch_head_hash == merge_base_hash { + println!("โœ… Method 4: Branch is fast-forward merged"); + return Ok(true); + } + } + + Ok(false) +} - if !merge_base.status.success() { - return Err(format!("Failed to find merge-base between '{branch_name}' and '{main_branch}'").into()); +fn is_branch_merged(branch_name: &str, main_branch: &str) -> Result> { + // Try multiple methods to detect if branch is merged + let methods = [ + check_branch_in_merged_list, + check_unmerged_commits, + check_remote_tracking_branches, + check_fast_forward_merge, + ]; + + for method in &methods { + if method(branch_name, main_branch)? { + return Ok(true); + } } - let merge_base_hash = String::from_utf8_lossy(&merge_base.stdout).trim().to_string(); + // If all methods failed, show debug info + println!("โŒ All merge detection methods indicate the branch has not been merged"); + + // Get unmerged commits for debugging + let unmerged_commits = run_git_with_fallback( + &["rev-list", &format!("origin/{main_branch}..{branch_name}")], + &["rev-list", &format!("{main_branch}..{branch_name}")], + ); + + if let Ok(output) = unmerged_commits { + if output.status.success() { + let unmerged_output_binding = String::from_utf8_lossy(&output.stdout); + let unmerged_output = unmerged_output_binding.trim(); + let commit_hashes: Vec<&str> = unmerged_output.lines().collect(); + + if !commit_hashes.is_empty() { + println!(" Unmerged commits found: {}", commit_hashes.len()); + println!(" Some unmerged commits:"); + + for (i, commit_hash) in commit_hashes.iter().take(3).enumerate() { + if commit_hash.trim().is_empty() { + continue; + } + + if let Ok(commit_msg) = Command::new("git") + .args(["log", "--format=%s", "-n", "1", commit_hash.trim()]) + .output() + { + let msg_binding = String::from_utf8_lossy(&commit_msg.stdout); + let msg = msg_binding.trim(); + println!(" {}. {} - {}", i + 1, &commit_hash.trim()[..8], msg); + } + } + + if commit_hashes.len() > 3 { + println!(" ... and {} more commits", commit_hashes.len() - 3); + } + } + } + } - // If the branch HEAD is the same as the merge-base, the branch is fully merged - Ok(branch_head_hash == merge_base_hash) + Ok(false) } fn pull_main_branch(main_branch: &str) -> Result<(), Box> { From e968d4ebed8e42014d917e34c35871e828c5d469 Mon Sep 17 00:00:00 2001 From: tiye Date: Mon, 22 Sep 2025 19:42:37 +0800 Subject: [PATCH 6/7] handle review issues --- Cargo.lock | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + README.md | 4 +- src/git.rs | 29 ++++--- 4 files changed, 252 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f8f32b..8526ba6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,12 +57,24 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "bytesize" version = "1.3.0" @@ -83,6 +95,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -124,6 +142,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -414,6 +452,7 @@ dependencies = [ "serde_json", "sysinfo", "walkdir", + "webbrowser", ] [[package]] @@ -422,6 +461,28 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.32" @@ -431,6 +492,16 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -543,6 +614,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "neli" version = "0.6.4" @@ -717,6 +794,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "rayon" version = "1.10.0" @@ -963,6 +1046,64 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wayland-client" version = "0.29.5" @@ -1022,6 +1163,33 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "web-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +dependencies = [ + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle", + "url", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1115,6 +1283,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1142,6 +1319,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1173,6 +1365,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1185,6 +1383,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1197,6 +1401,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1215,6 +1425,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1227,6 +1443,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1239,6 +1461,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1251,6 +1479,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index d59a470..55e9d2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,5 @@ serde = "1.0.216" serde_json = "1.0.133" colored = "2.1.0" git2 = "0.20.2" +webbrowser = "0.8" + diff --git a/README.md b/README.md index cfe7298..bcc6519 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ The `in br` command provides Git branch management functionality. ### Finish Branch -Finish the current branch by merging it to the main branch and cleaning up: +Finish the current branch by verifying it is already merged into the main branch and cleaning up: ```bash in br finish @@ -130,7 +130,7 @@ in br finish This command will: -- Check if the current branch is merged to the main branch +- Check if the current branch is already merged into the main branch; abort if it is not - Switch to the main branch - Pull the latest changes - Delete the feature branch diff --git a/src/git.rs b/src/git.rs index fb31e67..e066b37 100644 --- a/src/git.rs +++ b/src/git.rs @@ -103,7 +103,6 @@ pub fn finish_branch() -> Result<(), Box> { pub fn open_remote_repository() -> Result<(), String> { use std::fs; - use std::process::Command; // ่Žทๅ–ๅฝ“ๅ‰ๅˆ†ๆ”ฏๅ let repo = Repository::open(".").map_err(|e| format!("Failed to open repository: {e}"))?; @@ -172,17 +171,8 @@ pub fn open_remote_repository() -> Result<(), String> { println!("๐ŸŒ Opening remote repository: {web_url} (branch: {current_branch})"); // ไฝฟ็”จ็ณป็ปŸ้ป˜่ฎคๆต่งˆๅ™จๆ‰“ๅผ€ URL - let result = Command::new("open") - .arg(&web_url) - .output() - .map_err(|e| format!("Failed to open browser: {e}"))?; - - if !result.status.success() { - let stderr = String::from_utf8_lossy(&result.stderr); - return Err(format!("Failed to open URL: {stderr}")); - } + webbrowser::open(&web_url).map_err(|e| format!("Failed to open browser: {e}"))?; - println!("โœ… Successfully opened remote repository in browser"); Ok(()) } @@ -214,13 +204,22 @@ fn detect_main_branch(repo: &Repository) -> Result Result<(), Box> { - let branch = repo.find_branch(branch_name, BranchType::Local)?; - let branch_ref = branch.get(); - let commit = branch_ref.peel_to_commit()?; + if let Ok(local) = repo.find_branch(branch_name, BranchType::Local) { + let commit = local.get().peel_to_commit()?; + repo.checkout_tree(commit.as_object(), None)?; + repo.set_head(&format!("refs/heads/{branch_name}"))?; + return Ok(()); + } + // Fallback: create local branch from origin/ + let remote_ref = format!("refs/remotes/origin/{branch_name}"); + let rref = repo.find_reference(&remote_ref)?; + let commit = rref.peel_to_commit()?; + repo.branch(branch_name, &commit, false)?; + let mut new_local = repo.find_branch(branch_name, BranchType::Local)?; + new_local.set_upstream(Some(&format!("origin/{branch_name}")))?; repo.checkout_tree(commit.as_object(), None)?; repo.set_head(&format!("refs/heads/{branch_name}"))?; - Ok(()) } From ccbb0c18e03438799cb21912e8a8867f24a4cc4c Mon Sep 17 00:00:00 2001 From: tiye Date: Fri, 26 Sep 2025 16:02:10 +0800 Subject: [PATCH 7/7] get a jwt decode subcommand --- Cargo.lock | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/args.rs | 2 + src/jwt.rs | 70 +++++++++++++++++++ src/main.rs | 9 ++- 5 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 src/jwt.rs diff --git a/Cargo.lock b/Cargo.lock index 8526ba6..77fe800 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -193,6 +199,15 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-new" version = "0.5.9" @@ -290,6 +305,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "git2" version = "0.20.2" @@ -446,6 +474,7 @@ dependencies = [ "cli-clipboard", "colored", "git2", + "jsonwebtoken", "local-ip-address", "parse-size", "serde", @@ -472,7 +501,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.61", "walkdir", "windows-sys 0.45.0", ] @@ -502,6 +531,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -574,7 +618,7 @@ checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782" dependencies = [ "libc", "neli", - "thiserror", + "thiserror 1.0.61", "windows-sys 0.59.0", ] @@ -676,6 +720,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "objc" version = "0.2.7" @@ -745,6 +823,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -776,6 +864,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.92" @@ -820,6 +914,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "0.38.34" @@ -886,6 +994,18 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.16", + "time", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -969,7 +1089,16 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.61", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", ] [[package]] @@ -983,6 +1112,48 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -1013,6 +1184,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -1046,6 +1223,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.103" @@ -1509,7 +1692,7 @@ dependencies = [ "nix", "os_pipe", "tempfile", - "thiserror", + "thiserror 1.0.61", "tree_magic_mini", "wayland-client", "wayland-protocols", diff --git a/Cargo.toml b/Cargo.toml index 55e9d2e..27c7ecc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,5 @@ serde_json = "1.0.133" colored = "2.1.0" git2 = "0.20.2" webbrowser = "0.8" +jsonwebtoken = "9.3.1" diff --git a/src/args.rs b/src/args.rs index 93af325..096d857 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,3 +1,4 @@ +use crate::jwt::InspectForJwt; use argh::FromArgs; #[derive(FromArgs, PartialEq, Debug)] @@ -19,6 +20,7 @@ pub enum InspectionCommand { DirMark(InspectForDirMark), Branch(InspectForBranch), ShowTags(InspectForTags), + Jwt(InspectForJwt), } /// command for inspecting IP addresses. diff --git a/src/jwt.rs b/src/jwt.rs new file mode 100644 index 0000000..374ec82 --- /dev/null +++ b/src/jwt.rs @@ -0,0 +1,70 @@ +use argh::FromArgs; +use jsonwebtoken::{decode, decode_header, DecodingKey, Validation}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "jwt")] +/// JWT (JSON Web Token) utilities +pub struct InspectForJwt { + #[argh(subcommand)] + pub subcommand: JwtCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +pub enum JwtCommand { + Decode(DecodeArgs), +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "decode")] +/// Decode a JWT and display the header and payload +pub struct DecodeArgs { + #[argh(positional)] + /// the JWT to decode, or a file path containing the JWT + input: String, +} + +pub fn handle_jwt_command(command: JwtCommand) -> Result<(), String> { + match command { + JwtCommand::Decode(args) => decode_jwt(args), + } +} + +fn decode_jwt(args: DecodeArgs) -> Result<(), String> { + let token = if Path::new(&args.input).is_file() { + match fs::read_to_string(&args.input) { + Ok(contents) => contents.trim().to_string(), + Err(e) => { + return Err(format!("Failed to read file '{}': {}", &args.input, e)); + } + } + } else { + args.input + }; + + let header = match decode_header(&token) { + Ok(h) => h, + Err(e) => { + return Err(format!("Failed to decode token header: {e}")); + } + }; + + let mut validation = Validation::default(); + validation.insecure_disable_signature_validation(); + validation.validate_exp = false; + validation.validate_aud = false; + + let token_data = match decode::(&token, &DecodingKey::from_secret(&[]), &validation) { + Ok(t) => t, + Err(e) => { + return Err(format!("Failed to decode token: {e}")); + } + }; + + println!("Header: {}", serde_json::to_string_pretty(&header).unwrap()); + println!("Payload: {}", serde_json::to_string_pretty(&token_data.claims).unwrap()); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 1c30cfc..e5138d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod args; mod dir_marks; mod git; +mod jwt; mod show_file_size; mod tags; @@ -131,13 +132,19 @@ fn main() -> Result<(), String> { std::process::exit(1); } } - } + }, ShowTags(options) => { if let Err(e) = tags::show_tags(&options) { eprintln!("Error showing tags: {e}"); std::process::exit(1); } } + Jwt(options) => { + if let Err(e) = jwt::handle_jwt_command(options.subcommand) { + eprintln!("Error handling jwt command: {e}"); + std::process::exit(1); + } + } } Ok(())