Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 127 additions & 14 deletions src/command/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ use url::Url;

#[derive(Parser, Debug)]
pub struct PushArgs {
// TODO --force
/// repository, e.g. origin
#[clap(requires("refspec"))]
repository: Option<String>,
Expand All @@ -35,6 +34,10 @@ pub struct PushArgs {

#[clap(long, short = 'u', requires("refspec"), requires("repository"))]
set_upstream: bool,

/// force push to remote repository
#[clap(long, short = 'f')]
force: bool,
}

pub async fn execute(args: PushArgs) {
Expand Down Expand Up @@ -69,11 +72,13 @@ pub async fn execute(args: PushArgs) {
let repo_url = Config::get_remote_url(&repository).await;

let branch = args.refspec.unwrap_or(branch);
let commit_hash = Branch::find_branch(&branch, None)
.await
.unwrap()
.commit
.to_string();
let commit_hash = match Branch::find_branch(&branch, None).await {
Some(branch_info) => branch_info.commit.to_string(),
None => {
eprintln!("fatal: branch '{}' not found", branch);
return;
}
};

println!("pushing {branch}({commit_hash}) to {repository}({repo_url})");

Expand Down Expand Up @@ -101,6 +106,32 @@ pub async fn execute(args: PushArgs) {
return;
}

// Check if remote is ancestor of local (for fast-forward check)
let remote_sha1 = SHA1::from_str(&remote_hash).unwrap();
let local_sha1 = SHA1::from_str(&commit_hash).unwrap();
let can_fast_forward = if remote_sha1 == SHA1::default() {
true // New branch, always fast-forwardable
} else {
is_ancestor(&remote_sha1, &local_sha1)
};

// If remote has commits that local doesn't have and force is not specified, reject push
if !can_fast_forward && !args.force {
eprintln!("fatal: cannot push non-fast-forwardable reference");
eprintln!("hint: need to fetch and merge remote changes first");
return;
} else if !can_fast_forward && args.force {
// Force push case - only show warning when force is actually needed
println!(
"{}",
"warning: forcing update of remote reference (override history)".yellow()
);
println!(
"{}",
"warning: this may overwrite remote commits, use with caution".yellow()
);
}

let mut data = BytesMut::new();
add_pkt_line_string(
&mut data,
Expand Down Expand Up @@ -167,6 +198,9 @@ pub async fn execute(args: PushArgs) {

println!("{}", "Push success".green());

let remote_tracking_branch = format!("refs/remotes/{}/{}", repository, branch);
Branch::update_branch(&remote_tracking_branch, &commit_hash, None).await;

// set after push success
if args.set_upstream {
branch::set_upstream(&branch, &format!("{repository}/{branch}")).await;
Expand All @@ -186,7 +220,12 @@ fn collect_history_commits(commit_id: &SHA1) -> HashSet<SHA1> {
while let Some(commit) = queue.pop_front() {
commits.insert(commit);

let commit = Commit::load(&commit);
// Try to load the commit; if missing or corrupt, skip this path
let commit = match Commit::try_load(&commit) {
Some(c) => c,
None => continue,
};

for parent in commit.parent_commit_ids.iter() {
queue.push_back(*parent);
}
Expand All @@ -200,7 +239,10 @@ fn incremental_objs(local_ref: SHA1, remote_ref: SHA1) -> HashSet<Entry> {
// just fast-forward optimization
if remote_ref != SHA1::default() {
// remote exists
let mut commit = Commit::load(&local_ref);
let mut commit = match Commit::try_load(&local_ref) {
Some(c) => c,
None => return HashSet::new(), // If commit doesn't exist, return empty set
};
let mut commits = Vec::new();
let mut ok = true;
loop {
Expand All @@ -214,15 +256,28 @@ fn incremental_objs(local_ref: SHA1, remote_ref: SHA1) -> HashSet<Entry> {
break;
}
// update commit to it's only parent
commit = Commit::load(&commit.parent_commit_ids[0]);
commit = match Commit::try_load(&commit.parent_commit_ids[0]) {
Some(c) => c,
None => {
ok = false;
break;
}
};
}
if ok {
// fast-forward
let mut objs = HashSet::new();
commits.reverse(); // from old to new
for i in 0..commits.len() - 1 {
let old_tree = Commit::load(&commits[i]).tree_id;
let new_commit = Commit::load(&commits[i + 1]);
let old_commit = match Commit::try_load(&commits[i]) {
Some(c) => c,
None => continue,
};
let old_tree = old_commit.tree_id;
let new_commit = match Commit::try_load(&commits[i + 1]) {
Some(c) => c,
None => continue,
};
objs.extend(diff_tree_objs(Some(&old_tree), &new_commit.tree_id));
objs.insert(new_commit.into());
}
Expand All @@ -240,8 +295,11 @@ fn incremental_objs(local_ref: SHA1, remote_ref: SHA1) -> HashSet<Entry> {
}
let mut root_commit = None;

while let Some(commit) = queue.pop_front() {
let commit = Commit::load(&commit);
while let Some(commit_id) = queue.pop_front() {
let commit = match Commit::try_load(&commit_id) {
Some(c) => c,
None => continue,
};
let parents = &commit.parent_commit_ids;
if parents.is_empty() {
if root_commit.is_none() {
Expand All @@ -251,7 +309,11 @@ fn incremental_objs(local_ref: SHA1, remote_ref: SHA1) -> HashSet<Entry> {
}
}
for parent in parents.iter() {
let parent_tree = Commit::load(parent).tree_id;
let parent_commit = match Commit::try_load(parent) {
Some(c) => c,
None => continue,
};
let parent_tree = parent_commit.tree_id;
objs.extend(diff_tree_objs(Some(&parent_tree), &commit.tree_id));
if !exist_commits.contains(parent) && !visit.contains(parent) {
queue.push_back(*parent);
Expand All @@ -274,6 +336,40 @@ fn incremental_objs(local_ref: SHA1, remote_ref: SHA1) -> HashSet<Entry> {
objs
}

/// Check if ancestor is an ancestor of descendant
fn is_ancestor(ancestor: &SHA1, descendant: &SHA1) -> bool {
if ancestor == descendant {
return true;
}

let mut queue = VecDeque::new();
let mut visited = HashSet::new();

queue.push_back(*descendant);
visited.insert(*descendant);

while let Some(commit_id) = queue.pop_front() {
if &commit_id == ancestor {
return true;
}

// Try to load the commit; if missing or corrupt, skip this path
let commit = match Commit::try_load(&commit_id) {
Some(c) => c,
None => continue,
};

for parent_id in &commit.parent_commit_ids {
if !visited.contains(parent_id) {
visited.insert(*parent_id);
queue.push_back(*parent_id);
}
}
}

false
}

/// calc objects that in `new_tree` but not in `old_tree`
/// - if `old_tree` is None, return all objects in `new_tree` (include tree itself)
fn diff_tree_objs(old_tree: Option<&SHA1>, new_tree: &SHA1) -> HashSet<Entry> {
Expand Down Expand Up @@ -333,18 +429,35 @@ mod test {
assert_eq!(args.repository, None);
assert_eq!(args.refspec, None);
assert!(!args.set_upstream);
assert!(!args.force);

let args = vec!["push", "origin", "master"];
let args = PushArgs::parse_from(args);
assert_eq!(args.repository, Some("origin".to_string()));
assert_eq!(args.refspec, Some("master".to_string()));
assert!(!args.set_upstream);
assert!(!args.force);

let args = vec!["push", "-u", "origin", "master"];
let args = PushArgs::parse_from(args);
assert_eq!(args.repository, Some("origin".to_string()));
assert_eq!(args.refspec, Some("master".to_string()));
assert!(args.set_upstream);
assert!(!args.force);

let args = vec!["push", "--force", "origin", "master"];
let args = PushArgs::parse_from(args);
assert_eq!(args.repository, Some("origin".to_string()));
assert_eq!(args.refspec, Some("master".to_string()));
assert!(!args.set_upstream);
assert!(args.force);

let args = vec!["push", "-f", "origin", "master"];
let args = PushArgs::parse_from(args);
assert_eq!(args.repository, Some("origin".to_string()));
assert_eq!(args.refspec, Some("master".to_string()));
assert!(!args.set_upstream);
assert!(args.force);
}

#[test]
Expand Down
9 changes: 9 additions & 0 deletions src/utils/object_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub trait TreeExt {

pub trait CommitExt {
fn load(hash: &SHA1) -> Commit;
fn try_load(hash: &SHA1) -> Option<Commit>;
}

pub trait BlobExt {
Expand Down Expand Up @@ -67,6 +68,14 @@ impl CommitExt for Commit {
let commit_data = storage.get(hash).unwrap();
Commit::from_bytes(&commit_data, *hash).unwrap()
}

fn try_load(hash: &SHA1) -> Option<Commit> {
let storage = util::objects_storage();
storage
.get(hash)
.ok()
.and_then(|commit_data| Commit::from_bytes(&commit_data, *hash).ok())
}
}

impl BlobExt for Blob {
Expand Down