/* * libgit2 "pull" example - shows how to pull remote data into a local branch. * * Written by the libgit2 contributors * * To the extent possible under law, the author(s) have dedicated all copyright * and related and neighboring rights to this software to the public domain * worldwide. This software is distributed without any warranty. * * You should have received a copy of the CC0 Public Domain Dedication along * with this software. If not, see * . */ use clap::Parser; use git2::Repository; use std::io::{self, Write}; use std::str; #[derive(Parser)] struct Args { arg_remote: Option, arg_branch: Option, } fn do_fetch<'a>( repo: &'a git2::Repository, refs: &[&str], remote: &'a mut git2::Remote, ) -> Result, git2::Error> { let mut cb = git2::RemoteCallbacks::new(); // Print out our transfer progress. cb.transfer_progress(|stats| { if stats.received_objects() == stats.total_objects() { print!( "Resolving deltas {}/{}\r", stats.indexed_deltas(), stats.total_deltas() ); } else if stats.total_objects() > 0 { print!( "Received {}/{} objects ({}) in {} bytes\r", stats.received_objects(), stats.total_objects(), stats.indexed_objects(), stats.received_bytes() ); } io::stdout().flush().unwrap(); true }); let mut fo = git2::FetchOptions::new(); fo.remote_callbacks(cb); // Always fetch all tags. // Perform a download and also update tips fo.download_tags(git2::AutotagOption::All); println!("Fetching {} for repo", remote.name().unwrap()); remote.fetch(refs, Some(&mut fo), None)?; // If there are local objects (we got a thin pack), then tell the user // how many objects we saved from having to cross the network. let stats = remote.stats(); if stats.local_objects() > 0 { println!( "\rReceived {}/{} objects in {} bytes (used {} local \ objects)", stats.indexed_objects(), stats.total_objects(), stats.received_bytes(), stats.local_objects() ); } else { println!( "\rReceived {}/{} objects in {} bytes", stats.indexed_objects(), stats.total_objects(), stats.received_bytes() ); } let fetch_head = repo.find_reference("FETCH_HEAD")?; Ok(repo.reference_to_annotated_commit(&fetch_head)?) } fn fast_forward( repo: &Repository, lb: &mut git2::Reference, rc: &git2::AnnotatedCommit, ) -> Result<(), git2::Error> { let name = match lb.name() { Some(s) => s.to_string(), None => String::from_utf8_lossy(lb.name_bytes()).to_string(), }; let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id()); println!("{}", msg); lb.set_target(rc.id(), &msg)?; repo.set_head(&name)?; repo.checkout_head(Some( git2::build::CheckoutBuilder::default() // For some reason the force is required to make the working directory actually get updated // I suspect we should be adding some logic to handle dirty working directory states // but this is just an example so maybe not. .force(), ))?; Ok(()) } fn normal_merge( repo: &Repository, local: &git2::AnnotatedCommit, remote: &git2::AnnotatedCommit, ) -> Result<(), git2::Error> { let local_tree = repo.find_commit(local.id())?.tree()?; let remote_tree = repo.find_commit(remote.id())?.tree()?; let ancestor = repo .find_commit(repo.merge_base(local.id(), remote.id())?)? .tree()?; let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?; if idx.has_conflicts() { println!("Merge conflicts detected..."); repo.checkout_index(Some(&mut idx), None)?; return Ok(()); } let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?; // now create the merge commit let msg = format!("Merge: {} into {}", remote.id(), local.id()); let sig = repo.signature()?; let local_commit = repo.find_commit(local.id())?; let remote_commit = repo.find_commit(remote.id())?; // Do our merge commit and set current branch head to that commit. let _merge_commit = repo.commit( Some("HEAD"), &sig, &sig, &msg, &result_tree, &[&local_commit, &remote_commit], )?; // Set working tree to match head. repo.checkout_head(None)?; Ok(()) } fn do_merge<'a>( repo: &'a Repository, remote_branch: &str, fetch_commit: git2::AnnotatedCommit<'a>, ) -> Result<(), git2::Error> { // 1. do a merge analysis let analysis = repo.merge_analysis(&[&fetch_commit])?; // 2. Do the appropriate merge if analysis.0.is_fast_forward() { println!("Doing a fast forward"); // do a fast forward let refname = format!("refs/heads/{}", remote_branch); match repo.find_reference(&refname) { Ok(mut r) => { fast_forward(repo, &mut r, &fetch_commit)?; } Err(_) => { // The branch doesn't exist so just set the reference to the // commit directly. Usually this is because you are pulling // into an empty repository. repo.reference( &refname, fetch_commit.id(), true, &format!("Setting {} to {}", remote_branch, fetch_commit.id()), )?; repo.set_head(&refname)?; repo.checkout_head(Some( git2::build::CheckoutBuilder::default() .allow_conflicts(true) .conflict_style_merge(true) .force(), ))?; } }; } else if analysis.0.is_normal() { // do a normal merge let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?; normal_merge(&repo, &head_commit, &fetch_commit)?; } else { println!("Nothing to do..."); } Ok(()) } fn run(args: &Args) -> Result<(), git2::Error> { let remote_name = args.arg_remote.as_ref().map(|s| &s[..]).unwrap_or("origin"); let remote_branch = args.arg_branch.as_ref().map(|s| &s[..]).unwrap_or("master"); let repo = Repository::open(".")?; let mut remote = repo.find_remote(remote_name)?; let fetch_commit = do_fetch(&repo, &[remote_branch], &mut remote)?; do_merge(&repo, &remote_branch, fetch_commit) } fn main() { let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), } }