Allow pushing after staging.
parent
014a7d72e5
commit
c0d0c0d7ba
|
@ -20,7 +20,7 @@ packages:
|
||||||
- $HOME/.bash_profile
|
- $HOME/.bash_profile
|
||||||
- $HOME/.bashrc
|
- $HOME/.bashrc
|
||||||
home-manager:
|
home-manager:
|
||||||
- $HOME/home.nix
|
- $HOME/.config/nixpkgs/home.nix
|
||||||
homesync:
|
homesync:
|
||||||
- $HOME/.homesync.yml
|
- $HOME/.homesync.yml
|
||||||
- $HOME/.config/homesync/homesync.yml
|
- $HOME/.config/homesync/homesync.yml
|
||||||
|
|
461
src/git.rs
461
src/git.rs
|
@ -1,10 +1,11 @@
|
||||||
use super::{config::PathConfig, path};
|
use super::{config::PathConfig, path};
|
||||||
use git2::{
|
use git2::{
|
||||||
Branch, BranchType, Commit, Cred, DiffOptions, Direction, FetchOptions, Index, IndexAddOption,
|
BranchType, Commit, Cred, DiffOptions, Direction, FetchOptions, Index, IndexAddOption,
|
||||||
ObjectType, Remote, RemoteCallbacks, Repository, Signature,
|
ObjectType, PushOptions, Remote, RemoteCallbacks, Repository, Signature, StashApplyOptions,
|
||||||
|
StashFlags,
|
||||||
};
|
};
|
||||||
use path::ResPathBuf;
|
use path::ResPathBuf;
|
||||||
use simplelog::{info, paris};
|
use simplelog::{info, paris, warn};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
env::VarError,
|
env::VarError,
|
||||||
|
@ -76,20 +77,20 @@ impl error::Error for Error {}
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
fn clone(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
|
fn clone(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
|
||||||
let fetch_options = get_fetch_options(&pc)?;
|
let fetch_options = get_fetch_options(pc)?;
|
||||||
let mut builder = git2::build::RepoBuilder::new();
|
let mut builder = git2::build::RepoBuilder::new();
|
||||||
builder.fetch_options(fetch_options);
|
builder.fetch_options(fetch_options);
|
||||||
|
|
||||||
Ok(builder.clone(&pc.config.repos.remote.url, &expanded)?)
|
Ok(builder.clone(&pc.config.repos.remote.url, &expanded)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(jrpotter): Setup a sentinel file in the given repository. This is used
|
||||||
|
// for both ensuring any remote repositories are already managed by homesync and
|
||||||
|
// for storing any persisted configurations.
|
||||||
|
|
||||||
/// Sets up a local github repository all configuration files will be synced to.
|
/// Sets up a local github repository all configuration files will be synced to.
|
||||||
/// If there does not exist a local repository at the requested location, we
|
/// If there does not exist a local repository at the requested location, we
|
||||||
/// attempt to make it via cloning or initializing.
|
/// attempt to make it via cloning or initializing.
|
||||||
///
|
|
||||||
/// TODO(jrpotter): Setup a sentinel file in the given repository. This is used
|
|
||||||
/// for both ensuring any remote repositories are already managed by homesync
|
|
||||||
/// and for storing any persisted configurations.
|
|
||||||
pub fn init(pc: &PathConfig) -> Result<Repository> {
|
pub fn init(pc: &PathConfig) -> Result<Repository> {
|
||||||
// Permit the use of environment variables within the local configuration
|
// Permit the use of environment variables within the local configuration
|
||||||
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
|
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
|
||||||
|
@ -111,7 +112,7 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
|
||||||
);
|
);
|
||||||
Ok(repo)
|
Ok(repo)
|
||||||
}
|
}
|
||||||
Err(e) if e.code() == git2::ErrorCode::NotFound => match clone(&pc, &expanded) {
|
Err(e) if e.code() == git2::ErrorCode::NotFound => match clone(pc, &expanded) {
|
||||||
Ok(repo) => {
|
Ok(repo) => {
|
||||||
info!(
|
info!(
|
||||||
"Cloned remote repository <green>{}</>.",
|
"Cloned remote repository <green>{}</>.",
|
||||||
|
@ -120,7 +121,7 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
|
||||||
Ok(repo)
|
Ok(repo)
|
||||||
}
|
}
|
||||||
Err(Error::GitError(e))
|
Err(Error::GitError(e))
|
||||||
if e.code() == git2::ErrorCode::Eof && e.class() == git2::ErrorClass::Ssh =>
|
if e.class() == git2::ErrorClass::Ssh && e.code() == git2::ErrorCode::Eof =>
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"Creating local repository at <green>{}</>.",
|
"Creating local repository at <green>{}</>.",
|
||||||
|
@ -170,9 +171,9 @@ fn find_package_files(pc: &PathConfig) -> Vec<ResPathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
let workdir = validate_repo(&repo)?;
|
let workdir = check_working_repo(repo)?;
|
||||||
let repo_files = find_repo_files(&workdir)?;
|
let repo_files = find_repo_files(&workdir)?;
|
||||||
let package_files = find_package_files(&pc);
|
let package_files = find_package_files(pc);
|
||||||
|
|
||||||
// Find all files in our repository that are no longer being referenced in
|
// Find all files in our repository that are no longer being referenced in
|
||||||
// our primary config file. They should be removed from the repository.
|
// our primary config file. They should be removed from the repository.
|
||||||
|
@ -207,6 +208,11 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
fs::copy(package_file.resolved(), copy)?;
|
fs::copy(package_file.resolved(), copy)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Staged files. Run <italic>git -C <green>{}</> <italic>status</> to see what changed.",
|
||||||
|
&pc.config.repos.local.display()
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,16 +220,184 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
// Syncing
|
// Syncing
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
/// Adds all files to our index.
|
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
|
||||||
///
|
// First pull to make sure there are no conflicts when we push our changes.
|
||||||
/// Checks explicitly if any changes have been detected in the newly constructed
|
// This will also perform validation and construct our local and remote
|
||||||
/// index. If not, return `None`.
|
// environment.
|
||||||
pub fn index_add(repo: &Repository) -> Result<Option<Index>> {
|
pull(pc, repo)?;
|
||||||
|
|
||||||
|
let refspec = format!("refs/heads/{}", &pc.config.repos.remote.branch);
|
||||||
|
repo.set_head(&refspec)?;
|
||||||
|
|
||||||
|
// The index corresponds to our staging area. We add all files and write out
|
||||||
|
// to a tree. The resulting tree can be found using `git ls-tree <oid>`.
|
||||||
|
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||||
|
let mut index = match index_with_all(repo)? {
|
||||||
|
Some(index) => index,
|
||||||
|
None => {
|
||||||
|
warn!("Nothing to push. Have you run `homesync stage`?");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let index_oid = index.write_tree()?;
|
||||||
|
// Want to also reflect this change on the working directory.
|
||||||
|
index.write()?;
|
||||||
|
let index_tree = repo.find_tree(index_oid)?;
|
||||||
|
info!("Writing index to tree `{}`.", index_oid);
|
||||||
|
|
||||||
|
// Commit our changes and push them to our remote.
|
||||||
|
// TODO(jrpotter): Come up with a more useful message.
|
||||||
|
let signature = now_signature(pc)?;
|
||||||
|
let message = "Automated homesync commit.";
|
||||||
|
let commit_oid = if let Some(commit) = get_commit_at_head(repo) {
|
||||||
|
repo.commit(
|
||||||
|
Some(&refspec),
|
||||||
|
&signature,
|
||||||
|
&signature,
|
||||||
|
message,
|
||||||
|
&index_tree,
|
||||||
|
&[&commit],
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
repo.commit(
|
||||||
|
Some(&refspec),
|
||||||
|
&signature,
|
||||||
|
&signature,
|
||||||
|
message,
|
||||||
|
&index_tree,
|
||||||
|
&[],
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
info!("Commited `{}` with message \"{}\".", commit_oid, message);
|
||||||
|
|
||||||
|
let mut remote = find_remote(pc, repo)?;
|
||||||
|
let call_options = get_remote_callbacks(pc)?;
|
||||||
|
remote.connect_auth(Direction::Push, Some(call_options), None)?;
|
||||||
|
|
||||||
|
let mut push_options = get_push_options(pc)?;
|
||||||
|
remote.push(&[&format!("{r}:{r}", r = refspec)], Some(&mut push_options))?;
|
||||||
|
info!(
|
||||||
|
"Pushed changes to remote `{}`.",
|
||||||
|
pc.config.repos.remote.tracking_branch(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_from_remote(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
|
fetch_remote(pc, repo)?;
|
||||||
|
|
||||||
|
let tracking_branch = pc.config.repos.remote.tracking_branch();
|
||||||
|
let remote_branch = repo.find_branch(&tracking_branch, BranchType::Remote)?;
|
||||||
|
let remote_ref = repo.reference_to_annotated_commit(remote_branch.get())?;
|
||||||
|
|
||||||
|
// It should never be the case this function is called when the local branch
|
||||||
|
// exists. Keep `force` to `false` to catch any misuse here.
|
||||||
|
repo.branch_from_annotated_commit(&pc.config.repos.remote.branch, &remote_ref, false)?;
|
||||||
|
info!("Created new local branch from `{}`.", &tracking_branch);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_rebase_remote(pc: &PathConfig, repo: &Repository) -> Result<()> {
|
||||||
|
fetch_remote(pc, repo)?;
|
||||||
|
|
||||||
|
let tracking_branch = pc.config.repos.remote.tracking_branch();
|
||||||
|
let remote_branch = repo.find_branch(&tracking_branch, BranchType::Remote)?;
|
||||||
|
let remote_ref = repo.reference_to_annotated_commit(remote_branch.get())?;
|
||||||
|
|
||||||
|
// Our remote branch after fetching should exist at the fetch. We could just
|
||||||
|
// rebase onto the remote branch directly, but let's keep things local when
|
||||||
|
// we can.
|
||||||
|
let local_branch = repo.find_branch(&pc.config.repos.remote.branch, BranchType::Local)?;
|
||||||
|
let local_ref = repo.reference_to_annotated_commit(local_branch.get())?;
|
||||||
|
|
||||||
|
let signature = now_signature(pc)?;
|
||||||
|
repo.rebase(Some(&local_ref), Some(&remote_ref), None, None)?
|
||||||
|
.finish(Some(&signature))?;
|
||||||
|
info!("Rebased local branch onto `{}`.", &tracking_branch);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
|
||||||
|
check_working_repo(repo)?;
|
||||||
|
|
||||||
|
// If our local branch exists, it must also have a commit on it. Therefore
|
||||||
|
// we can apply stashes. Stow away our changes, rebase on remote, and then
|
||||||
|
// reapply those changes.
|
||||||
|
if repo
|
||||||
|
.find_branch(&pc.config.repos.remote.branch, BranchType::Local)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Ok(with_stash(pc, repo, |pc, repo| {
|
||||||
|
Ok(local_rebase_remote(pc, repo)?)
|
||||||
|
})?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our local branch does not exist yet, we are likely in an empty git
|
||||||
|
// repository. In this case, we should just try to find the remote branch
|
||||||
|
// and establish a copy locally of the same name.
|
||||||
|
//
|
||||||
|
// That said, there is a possibility our repository isn't empty. We also are
|
||||||
|
// not necessarily able to stash changes and reapply them like we normally
|
||||||
|
// would since its possible we do not have an initial commit yet. Generally
|
||||||
|
// switching would be fine but its also possible the user has a file that
|
||||||
|
// would be overwritten on change. For this reason, we just create an
|
||||||
|
// initial commit for any existing files so the user can reference it later
|
||||||
|
// if need be.
|
||||||
|
if let Some(mut index) = index_with_all(repo)? {
|
||||||
|
let index_oid = index.write_tree()?;
|
||||||
|
let index_tree = repo.find_tree(index_oid)?;
|
||||||
|
info!("Writing tree `{}`.", index_oid);
|
||||||
|
|
||||||
|
let signature = now_signature(pc)?;
|
||||||
|
let message = "Save potentially conflicting files here.";
|
||||||
|
// If we are on a current branch, there should exist a commit we
|
||||||
|
// can just push onto. Otherwise let's create a new branch with the
|
||||||
|
// saved contents.
|
||||||
|
if let Some(parent_commit) = get_commit_at_head(repo) {
|
||||||
|
repo.commit(
|
||||||
|
Some("HEAD"),
|
||||||
|
&signature,
|
||||||
|
&signature,
|
||||||
|
message,
|
||||||
|
&index_tree,
|
||||||
|
&[&parent_commit],
|
||||||
|
)?;
|
||||||
|
info!("Saved potentially conflicting files in new commit of HEAD.");
|
||||||
|
} else {
|
||||||
|
let temp_branch = temporary_branch_name(pc, repo)?;
|
||||||
|
let refspec = format!("refs/heads/{}", &temp_branch);
|
||||||
|
repo.commit(
|
||||||
|
Some(&refspec),
|
||||||
|
&signature,
|
||||||
|
&signature,
|
||||||
|
message,
|
||||||
|
&index_tree,
|
||||||
|
&[],
|
||||||
|
)?;
|
||||||
|
info!(
|
||||||
|
"Saved potentially conflicting files on branch <yellow>{}</>",
|
||||||
|
temp_branch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(local_from_remote(pc, repo)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Index
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
fn index_with_all(repo: &Repository) -> Result<Option<Index>> {
|
||||||
let mut index = repo.index()?;
|
let mut index = repo.index()?;
|
||||||
index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?;
|
index.add_all(["."].iter(), IndexAddOption::DEFAULT, None)?;
|
||||||
|
let has_diff = if let Some(commit) = get_commit_at_head(repo) {
|
||||||
let diff_stats = repo
|
let diff_stats = repo
|
||||||
.diff_index_to_workdir(
|
.diff_tree_to_workdir_with_index(
|
||||||
Some(&index),
|
Some(&repo.find_tree(commit.tree_id())?),
|
||||||
Some(
|
Some(
|
||||||
DiffOptions::new()
|
DiffOptions::new()
|
||||||
.include_untracked(true)
|
.include_untracked(true)
|
||||||
|
@ -231,27 +405,83 @@ pub fn index_add(repo: &Repository) -> Result<Option<Index>> {
|
||||||
),
|
),
|
||||||
)?
|
)?
|
||||||
.stats()?;
|
.stats()?;
|
||||||
if diff_stats.files_changed() == 0
|
diff_stats.files_changed() != 0
|
||||||
&& diff_stats.insertions() == 0
|
|| diff_stats.insertions() != 0
|
||||||
&& diff_stats.deletions() == 0
|
|| diff_stats.deletions() != 0
|
||||||
{
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
} else {
|
||||||
|
!index.is_empty()
|
||||||
|
};
|
||||||
|
if has_diff {
|
||||||
Ok(Some(index))
|
Ok(Some(index))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create or retrieve the remote specified within our configuration.
|
fn with_stash<T>(pc: &PathConfig, repo: &mut Repository, function: T) -> Result<()>
|
||||||
///
|
where
|
||||||
/// This method also configures the fetchspec for the remote, explicitly mapping
|
T: Fn(&PathConfig, &mut Repository) -> Result<()>,
|
||||||
/// the remote branch against our local one.
|
{
|
||||||
///
|
let signature = now_signature(pc)?;
|
||||||
/// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
|
let stash_oid = match repo.stash_save(
|
||||||
fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
|
&signature,
|
||||||
|
"Temporary stash during pull",
|
||||||
|
Some(StashFlags::INCLUDE_UNTRACKED),
|
||||||
|
) {
|
||||||
|
Ok(oid) => {
|
||||||
|
info!("Stashing changes in `{}`.", oid);
|
||||||
|
Some(oid)
|
||||||
|
}
|
||||||
|
Err(e) if e.class() == git2::ErrorClass::Stash && e.code() == git2::ErrorCode::NotFound => {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(e) => Err(e)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
function(pc, repo)?;
|
||||||
|
|
||||||
|
if let Some(oid) = stash_oid {
|
||||||
|
// It is possible something else made changes to our stash while we were
|
||||||
|
// rebasing. To be extra cautious, search for our specific stash
|
||||||
|
// instance.
|
||||||
|
let mut stash_index = None;
|
||||||
|
repo.stash_foreach(|index, _message, each_oid| {
|
||||||
|
if *each_oid == oid {
|
||||||
|
stash_index = Some(index);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if let Some(index) = stash_index {
|
||||||
|
let mut checkout = git2::build::CheckoutBuilder::new();
|
||||||
|
checkout.use_ours(true);
|
||||||
|
|
||||||
|
let mut apply_options = StashApplyOptions::new();
|
||||||
|
apply_options.checkout_options(checkout);
|
||||||
|
|
||||||
|
repo.stash_apply(index, Some(&mut apply_options))?;
|
||||||
|
info!("Reapplied stash `{}`.", oid);
|
||||||
|
} else {
|
||||||
|
warn!("Could not find stash `{}`. Ignoring.", oid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Remote
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
fn find_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
|
||||||
repo.remote_set_url(&pc.config.repos.remote.name, &pc.config.repos.remote.url)?;
|
repo.remote_set_url(&pc.config.repos.remote.name, &pc.config.repos.remote.url)?;
|
||||||
|
// If the remote already exists, this just updates the fetchspec. We could
|
||||||
|
// go with "*" instead of {branch} for all remote branches, but choosing to
|
||||||
|
// be precise..
|
||||||
|
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
|
||||||
repo.remote_add_fetch(
|
repo.remote_add_fetch(
|
||||||
&pc.config.repos.remote.name,
|
&pc.config.repos.remote.name,
|
||||||
// We could go with "*" instead of {branch} for all remote branches.
|
|
||||||
&format!(
|
&format!(
|
||||||
"+refs/heads/{}:refs/remotes/{}",
|
"+refs/heads/{}:refs/remotes/{}",
|
||||||
pc.config.repos.remote.branch,
|
pc.config.repos.remote.branch,
|
||||||
|
@ -261,119 +491,21 @@ fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<
|
||||||
Ok(repo.find_remote(&pc.config.repos.remote.name)?)
|
Ok(repo.find_remote(&pc.config.repos.remote.name)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
|
fn fetch_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
|
||||||
// First pull to make sure there are no conflicts when we push our changes.
|
let mut remote = find_remote(pc, repo)?;
|
||||||
// This will also perform validation and construct our local and remote
|
let mut fetch_options = get_fetch_options(pc)?;
|
||||||
// environment.
|
|
||||||
let _local_branch = pull(&pc, &repo)?;
|
|
||||||
|
|
||||||
// The index corresponds to our staging area. We add all files and write out
|
|
||||||
// to a tree. The resulting tree can be found using `git ls-tree <oid>`.
|
|
||||||
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
|
||||||
let mut index = match index_add(&repo)? {
|
|
||||||
Some(index) => index,
|
|
||||||
None => {
|
|
||||||
info!("Nothing to push. Have you run `homesync stage`?");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Retrieve the latest commit before writing to the object database.
|
|
||||||
let parent_commit = get_commit(&repo)?;
|
|
||||||
let index_oid = index.write_tree()?;
|
|
||||||
let index_tree = repo.find_tree(index_oid)?;
|
|
||||||
info!("Writing index to tree `{}`.", index_oid);
|
|
||||||
|
|
||||||
// Commit our changes and push them to our remote.
|
|
||||||
let refspec = format!("refs/heads/{}", &pc.config.repos.remote.branch);
|
|
||||||
let signature = get_signature(&pc)?;
|
|
||||||
repo.commit(
|
|
||||||
Some(&refspec),
|
|
||||||
&signature,
|
|
||||||
&signature,
|
|
||||||
// TODO(jrpotter): Come up with a more useful message.
|
|
||||||
"homesync push",
|
|
||||||
&index_tree,
|
|
||||||
&[&parent_commit],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut remote = get_remote(&pc, &repo)?;
|
|
||||||
remote.connect(Direction::Push)?;
|
|
||||||
remote.push(&[&format!("{r}:{r}", r = refspec)], None)?;
|
|
||||||
info!(
|
|
||||||
"Pushed changes to remote `{}`.",
|
|
||||||
pc.config.repos.remote.tracking_branch(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pull<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Branch<'repo>> {
|
|
||||||
validate_repo(&repo)?;
|
|
||||||
|
|
||||||
// TODO(jrpotter): Configure our remote to point to the same URL mentioned
|
|
||||||
// in our config.
|
|
||||||
// TODO(jrpotter): If changes are available, need to stage them and then
|
|
||||||
// reapply.
|
|
||||||
|
|
||||||
// Establish our remote. If the remote already exists, re-configure it
|
|
||||||
// blindly to point to the appropriate url. Our results should now exist
|
|
||||||
// in a branch called `remotes/origin/<branch>`.
|
|
||||||
// https://git-scm.com/book/it/v2/Git-Basics-Working-with-Remotes
|
|
||||||
let mut remote = get_remote(&pc, &repo)?;
|
|
||||||
let mut fetch_options = get_fetch_options(&pc)?;
|
|
||||||
remote.fetch(
|
remote.fetch(
|
||||||
&[&pc.config.repos.remote.branch],
|
&[&pc.config.repos.remote.branch],
|
||||||
Some(&mut fetch_options),
|
Some(&mut fetch_options),
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let remote_branch_name = pc.config.repos.remote.tracking_branch();
|
let tracking_branch = pc.config.repos.remote.tracking_branch();
|
||||||
let remote_branch = repo.find_branch(&remote_branch_name, BranchType::Remote)?;
|
info!("Fetched remote branch `{}`.", &tracking_branch);
|
||||||
info!("Fetched remote branch `{}`.", &remote_branch_name);
|
|
||||||
|
|
||||||
// There are two cases we need to consider:
|
Ok(remote)
|
||||||
//
|
|
||||||
// 1. Our local branch actually exists, in which case there are commits
|
|
||||||
// available. These should be rebased relative to remote (our upstream).
|
|
||||||
// 2. Our repository has been initialized in an empty state. The branch we
|
|
||||||
// are interested in is unborn, so we can just copy the branch from remote.
|
|
||||||
let remote_ref = repo.reference_to_annotated_commit(remote_branch.get())?;
|
|
||||||
if let Ok(local_branch) = repo.find_branch(&pc.config.repos.remote.branch, BranchType::Local) {
|
|
||||||
let local_ref = repo.reference_to_annotated_commit(local_branch.get())?;
|
|
||||||
let signature = get_signature(&pc)?;
|
|
||||||
repo.rebase(Some(&local_ref), Some(&remote_ref), None, None)?
|
|
||||||
.finish(Some(&signature))?;
|
|
||||||
info!("Rebased local branch onto `{}`.", remote_branch_name);
|
|
||||||
Ok(local_branch)
|
|
||||||
} else {
|
|
||||||
let local_branch =
|
|
||||||
repo.branch_from_annotated_commit(&pc.config.repos.remote.branch, &remote_ref, false)?;
|
|
||||||
info!("Created new local branch from `{}`.", remote_branch_name);
|
|
||||||
Ok(local_branch)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
fn get_remote_callbacks(pc: &PathConfig) -> Result<RemoteCallbacks> {
|
||||||
// Utility
|
|
||||||
// ========================================
|
|
||||||
|
|
||||||
/// Verify the repository we are working in supports the operations we want to
|
|
||||||
/// apply to it.
|
|
||||||
fn validate_repo(repo: &Repository) -> Result<PathBuf> {
|
|
||||||
Ok(repo.workdir().ok_or(Error::InvalidBareRepo)?.to_path_buf())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the latest commit off of HEAD.
|
|
||||||
fn get_commit(repo: &Repository) -> Result<Commit> {
|
|
||||||
Ok(repo
|
|
||||||
.head()?
|
|
||||||
.resolve()?
|
|
||||||
.peel(ObjectType::Commit)?
|
|
||||||
.into_commit()
|
|
||||||
.map_err(|_| git2::Error::from_str("Couldn't find commit"))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct callbacks to supply authentication information on fetch/clone/etc.
|
|
||||||
fn get_fetch_options(pc: &PathConfig) -> Result<FetchOptions> {
|
|
||||||
let public_path = match &pc.config.ssh.public {
|
let public_path = match &pc.config.ssh.public {
|
||||||
Some(p) => Some(path::resolve(p)?),
|
Some(p) => Some(path::resolve(p)?),
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -390,12 +522,63 @@ fn get_fetch_options(pc: &PathConfig) -> Result<FetchOptions> {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Ok(callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_fetch_options(pc: &PathConfig) -> Result<FetchOptions> {
|
||||||
|
let callbacks = get_remote_callbacks(pc)?;
|
||||||
let mut fetch_options = FetchOptions::new();
|
let mut fetch_options = FetchOptions::new();
|
||||||
fetch_options.remote_callbacks(callbacks);
|
fetch_options.remote_callbacks(callbacks);
|
||||||
Ok(fetch_options)
|
Ok(fetch_options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a new signature at the current time.
|
fn get_push_options(pc: &PathConfig) -> Result<PushOptions> {
|
||||||
fn get_signature(pc: &PathConfig) -> Result<Signature> {
|
let callbacks = get_remote_callbacks(pc)?;
|
||||||
|
let mut push_options = PushOptions::new();
|
||||||
|
push_options.remote_callbacks(callbacks);
|
||||||
|
Ok(push_options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Miscellaneous
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
fn check_working_repo(repo: &Repository) -> Result<PathBuf> {
|
||||||
|
Ok(repo.workdir().ok_or(Error::InvalidBareRepo)?.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_commit_at_head(repo: &Repository) -> Option<Commit> {
|
||||||
|
let peel = || -> Result<Commit> {
|
||||||
|
Ok(repo
|
||||||
|
.head()?
|
||||||
|
.resolve()?
|
||||||
|
.peel(ObjectType::Commit)?
|
||||||
|
.into_commit()
|
||||||
|
.map_err(|_| git2::Error::from_str("Couldn't find commit"))?)
|
||||||
|
};
|
||||||
|
peel().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_signature(pc: &PathConfig) -> Result<Signature> {
|
||||||
Ok(Signature::now(&pc.config.user.name, &pc.config.user.email)?)
|
Ok(Signature::now(&pc.config.user.name, &pc.config.user.email)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn temporary_branch_name(pc: &PathConfig, repo: &Repository) -> Result<String> {
|
||||||
|
let mut branch_names = HashSet::new();
|
||||||
|
for b in repo.branches(Some(BranchType::Local))? {
|
||||||
|
if let Ok((branch, _branch_type)) = b {
|
||||||
|
if let Some(name) = branch.name()? {
|
||||||
|
branch_names.insert(name.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut count = 1;
|
||||||
|
let mut temp_name = format!("{}-tmp", &pc.config.repos.remote.branch);
|
||||||
|
while branch_names.contains(&temp_name) {
|
||||||
|
temp_name = format!("{}-tmp-{}", &pc.config.repos.remote.branch, count);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(temp_name)
|
||||||
|
}
|
||||||
|
|
|
@ -26,8 +26,8 @@ pub fn run_push(config: PathConfig) -> Result {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_pull(config: PathConfig) -> Result {
|
pub fn run_pull(config: PathConfig) -> Result {
|
||||||
let repo = git::init(&config)?;
|
let mut repo = git::init(&config)?;
|
||||||
git::pull(&config, &repo)?;
|
git::pull(&config, &mut repo)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue