Skip to content
Merged
55 changes: 50 additions & 5 deletions libra/src/command/add.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::command::status;
use crate::utils::object_ext::BlobExt;
use clap::Parser;
use clap::{Parser};
use mercury::internal::index::{Index, IndexEntry};
use mercury::internal::object::blob::Blob;
use std::path::{Path, PathBuf};
Expand All @@ -24,6 +24,14 @@ pub struct AddArgs {
#[clap(short, long, group = "mode")]
pub update: bool,

/// Refresh index entries for all files currently in the index.
///
/// This updates only the metadata (e.g. file stat information such as
/// timestamps, file size, etc.) of existing index entries to match
/// the working tree, without adding new files or removing entries.
#[clap(long, group = "mode")]
pub refresh: bool,

/// more detailed output
#[clap(short, long)]
pub verbose: bool,
Expand Down Expand Up @@ -64,6 +72,42 @@ pub async fn execute(args: AddArgs) {
changes.deleted = util::filter_to_fit_paths(&changes.deleted, &paths);
}

if args.refresh {
let index_path = path::index();
let index = Index::load(&index_path).unwrap();

Copilot AI Jul 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index is loaded twice in the refresh block (lines 77 and 93). Consider loading it once and reusing the same instance to avoid redundant I/O operations.

Suggested change
let index = Index::load(&index_path).unwrap();
let mut index = Index::load(&index_path).unwrap();

Copilot uses AI. Check for mistakes.
Comment thread
AidCheng marked this conversation as resolved.

let files: Vec<PathBuf> = changes
.modified
.into_iter()
.filter(|p| {
let s = p.to_str().unwrap_or_else(|| { panic!("path {:?} is not valid UTF-8", p.display()) });
index.tracked(s, 0)
})
.collect();

// check for dry_run
if args.dry_run {
for file in &files {
println!("refresh: {}", file.display())
}
}

let index_file = path::index();
let mut index = Index::load(&index_file).unwrap();
for file in &files {
if index
.refresh(file, &util::working_dir())
.unwrap_or_else(|_| panic!("error refreshing {}", file.display()))
&& args.verbose
{
println!("refreshed: {}", file.display());
}
}

index.save(&index_file).unwrap();
return;
}

let mut files = changes.modified;
files.extend(changes.deleted);
// `--update` only operates on tracked files, not including `new` files
Expand Down Expand Up @@ -203,9 +247,10 @@ mod test {
use super::*;

#[test]
#[should_panic]
/// Test that `-A` and `-u` cannot be used together
fn test_args_parse_update_conflict_with_all() {
AddArgs::try_parse_from(["test", "-A", "-u"]).unwrap();
fn test_args_conflict_with_refresh() {
// "--refresh" cannot be combined with "-A", "--refresh" or "-u"
assert!(AddArgs::try_parse_from(["test", "-A", "--refresh"]).is_err());
assert!(AddArgs::try_parse_from(["test", "-u", "--refresh"]).is_err());
assert!(AddArgs::try_parse_from(["test", "-A", "-u", "--refresh"]).is_err());
}
}
8 changes: 8 additions & 0 deletions libra/tests/command/cherry_pick_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async fn test_basic_cherry_pick() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -61,6 +62,7 @@ async fn test_basic_cherry_pick() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -88,6 +90,7 @@ async fn test_basic_cherry_pick() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -217,6 +220,7 @@ async fn test_cherry_pick_with_commit() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -245,6 +249,7 @@ async fn test_cherry_pick_with_commit() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -320,6 +325,7 @@ async fn test_cherry_pick_multiple_commits() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -349,6 +355,7 @@ async fn test_cherry_pick_multiple_commits() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand All @@ -371,6 +378,7 @@ async fn test_cherry_pick_multiple_commits() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down
1 change: 1 addition & 0 deletions libra/tests/command/commit_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ async fn test_execute_commit() {
pathspec: vec![],
dry_run: false,
ignore_errors: false,
refresh: false,
};
add::execute(args).await;
}
Expand Down
7 changes: 7 additions & 0 deletions libra/tests/command/rebase_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async fn test_basic_rebase() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand All @@ -41,6 +42,7 @@ async fn test_basic_rebase() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -70,6 +72,7 @@ async fn test_basic_rebase() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand All @@ -90,6 +93,7 @@ async fn test_basic_rebase() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -118,6 +122,7 @@ async fn test_basic_rebase() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -187,6 +192,7 @@ async fn test_rebase_already_up_to_date() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand All @@ -207,6 +213,7 @@ async fn test_rebase_already_up_to_date() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down
5 changes: 5 additions & 0 deletions libra/tests/command/reset_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1,
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -47,6 +48,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1,
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -78,6 +80,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1,
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -109,6 +112,7 @@ async fn setup_standard_repo(temp_path: &std::path::Path) -> (SHA1, SHA1, SHA1,
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -149,6 +153,7 @@ async fn setup_test_state() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
}
Expand Down
6 changes: 6 additions & 0 deletions libra/tests/command/revert_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async fn test_basic_revert() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh : false,
})
.await;
commit::execute(CommitArgs {
Expand All @@ -52,6 +53,7 @@ async fn test_basic_revert() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand All @@ -75,6 +77,7 @@ async fn test_basic_revert() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -172,6 +175,7 @@ async fn test_revert_no_commit() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand All @@ -192,6 +196,7 @@ async fn test_revert_no_commit() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down Expand Up @@ -250,6 +255,7 @@ async fn test_revert_root_commit() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;
commit::execute(CommitArgs {
Expand Down
1 change: 1 addition & 0 deletions libra/tests/command/status_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async fn test_changes_to_be_staged() {
verbose: false,
dry_run: false,
ignore_errors: false,
refresh: false,
})
.await;

Expand Down
1 change: 1 addition & 0 deletions libra/tests/command/switch_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async fn test_check_status() {
verbose: true,
dry_run: false,
ignore_errors: false,
refresh: false,
};
add::execute(add_args).await;
assert!(check_status().await);
Expand Down
50 changes: 50 additions & 0 deletions mercury/src/internal/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,56 @@ impl Index {
file.write_all(&file_hash)?;
Ok(())
}

pub fn refresh(&mut self, file: impl AsRef<Path>, workdir: &Path) -> Result<bool, GitError> {
let path = file.as_ref();
let name = path.to_str()
.ok_or(GitError::InvalidPathError(format!("{path:?}")))?;

if let Some(entry) = self.entries.get_mut(&(name.to_string(), 0)) {
let abs_path = workdir.join(path);
let meta = fs::symlink_metadata(&abs_path)?;
// Try creation time; on error, warn and use modification time (or now)
let new_ctime = Time::from_system_time(Self::time_or_now("creation time", &abs_path, meta.created()));
let new_mtime = Time::from_system_time(Self::time_or_now("modification time", &abs_path, meta.modified()));
let new_size = meta.len() as u32;

// re-calculate SHA1
let mut file = File::open(&abs_path)?;
let mut hasher = Sha1::new();
io::copy(&mut file, &mut hasher)?;
let new_hash = SHA1::from_bytes(&hasher.finalize());

// refresh index
if entry.ctime != new_ctime
|| entry.mtime != new_mtime
|| entry.size != new_size
|| entry.hash != new_hash
{
entry.ctime = new_ctime;
entry.mtime = new_mtime;
entry.size = new_size;
entry.hash = new_hash;
return Ok(true);
}
}
Ok(false)
}

/// Try to get a timestamp, logging on error, and finally falling back to now.
fn time_or_now(
what: &str,
path: &Path,
res: io::Result<SystemTime>,
) -> SystemTime {
match res {
Ok(ts) => ts,
Err(e) => {
eprintln!("warning: failed to get {what} for {path:?}: {e}; using SystemTime::now()", what = what, path = path.display());
SystemTime::now()
}
}
}
}

impl Index {
Expand Down
Loading