Add idea on pull command.

pull/3/head
Joshua Potter 2022-01-07 08:45:05 -05:00
parent 258ac51b51
commit 53f0b399c0
3 changed files with 140 additions and 60 deletions

View File

@ -1,7 +1,10 @@
use super::{config::PathConfig, path}; use super::{config::PathConfig, path};
use git2::{IndexAddOption, ObjectType, Remote, Repository, Signature}; use git2::{
Branch, BranchType, Commit, IndexAddOption, ObjectType, Reference, Remote, Repository,
Signature,
};
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,
@ -20,8 +23,7 @@ pub type Result<T> = result::Result<T, Error>;
pub enum Error { pub enum Error {
GitError(git2::Error), GitError(git2::Error),
IOError(io::Error), IOError(io::Error),
NotHomesyncRepo, InvalidBareRepo,
NotWorkingRepo,
VarError(VarError), VarError(VarError),
} }
@ -57,11 +59,7 @@ impl fmt::Display for Error {
match self { match self {
Error::GitError(e) => write!(f, "{}", e), Error::GitError(e) => write!(f, "{}", e),
Error::IOError(e) => write!(f, "{}", e), Error::IOError(e) => write!(f, "{}", e),
Error::NotHomesyncRepo => write!( Error::InvalidBareRepo => write!(
f,
"Local repository is not managed by `homesync`. Missing `.homesync` sentinel file."
),
Error::NotWorkingRepo => write!(
f, f,
"Local repository should be a working directory. Did you manually initialize with `--bare`?" "Local repository should be a working directory. Did you manually initialize with `--bare`?"
), ),
@ -76,12 +74,39 @@ impl error::Error for Error {}
// Initialization // Initialization
// ======================================== // ========================================
fn clone_or_init(pc: &PathConfig, expanded: &Path) -> Result<Repository> {
match Repository::clone(&pc.config.remote.url.to_string(), &expanded) {
Ok(repo) => {
info!(
"Cloned remote repository <green>{}</>.",
&pc.config.remote.url
);
Ok(repo)
}
Err(e) if (e.code() == git2::ErrorCode::NotFound || e.code() == git2::ErrorCode::Auth) => {
// TODO(jrpotter): Setup authentication callbacks so private
// repositories work.
// https://docs.rs/git2/0.13.25/git2/build/struct.RepoBuilder.html#example
if e.code() == git2::ErrorCode::Auth {
warn!("Could not authenticate against remote. Are you using a public repository?");
}
info!(
"Creating local repository at <green>{}</>.",
pc.config.local.display()
);
Ok(Repository::init(&expanded)?)
}
Err(e) => Err(e)?,
}
}
/// 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. /// attempt to make it via cloning or initializing.
/// ///
/// NOTE! This does not perform any syncing between local and remote. In fact, /// TODO(jrpotter): Setup a sentinel file in the given repository. This is used
/// this method does not perform any validation on remote at all. /// 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
@ -94,40 +119,17 @@ pub fn init(pc: &PathConfig) -> Result<Repository> {
// - the directory is not git-initialized (i.e. has a valid `.git` // - the directory is not git-initialized (i.e. has a valid `.git`
// subfolder). // subfolder).
// - the directory does not have appropriate permissions. // - the directory does not have appropriate permissions.
let local = match Repository::open(&expanded) { // - the remote repository is not found
Ok(repo) => Some(repo), match Repository::open(&expanded) {
Err(e) => match e.code() { Ok(repo) => {
git2::ErrorCode::NotFound => None,
_ => Err(e)?,
},
};
// 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.
let mut sentinel = PathBuf::from(&expanded);
sentinel.push(".homesync");
match local {
Some(repo) => {
// Verify the given repository has a homesync sentinel file.
match path::validate_is_file(&sentinel) {
Ok(_) => (),
Err(_) => Err(Error::NotHomesyncRepo)?,
};
Ok(repo)
}
// If no local repository exists, we choose to just always initialize a
// new one instead of cloning from remote. Cloning has a separate set of
// issues that we need to resolve anyways (e.g. setting remote, pulling,
// managing possible merge conflicts, etc.).
None => {
info!( info!(
"Creating new homesync repository at <green>{}</>.", "Opened local repository <green>{}</>.",
pc.config.local.display() &pc.config.local.display()
); );
let repo = Repository::init(&expanded)?;
fs::File::create(sentinel)?;
Ok(repo) Ok(repo)
} }
Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(clone_or_init(&pc, &expanded)?),
Err(e) => Err(e)?,
} }
} }
@ -167,7 +169,7 @@ 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 = repo.workdir().ok_or(Error::NotWorkingRepo)?; let workdir = repo.workdir().ok_or(Error::InvalidBareRepo)?;
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
@ -208,24 +210,8 @@ pub fn stage(pc: &PathConfig, repo: &Repository) -> Result<()> {
// Syncing // Syncing
// ======================================== // ========================================
fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
// Sets a new remote if it does not yet exist.
repo.remote_set_url(&pc.config.remote.name, &pc.config.remote.url.to_string())?;
// We could go with "*" instead of referencing the one branch, but let's be
// specific for the time being.
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
repo.remote_add_fetch(
&pc.config.remote.name,
&format!(
"+refs/heads/{branch}:refs/remotes/origin/{branch}",
branch = pc.config.remote.branch
),
)?;
Ok(repo.find_remote(&pc.config.remote.name)?)
}
pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> { pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
repo.workdir().ok_or(Error::NotWorkingRepo)?; repo.workdir().ok_or(Error::InvalidBareRepo)?;
// Switch to the new branch we want to work on. If the branch does not // Switch to the new branch we want to work on. If the branch does not
// exist, `set_head` will point to an unborn branch. // exist, `set_head` will point to an unborn branch.
// https://git-scm.com/docs/git-check-ref-format. // https://git-scm.com/docs/git-check-ref-format.
@ -280,3 +266,89 @@ pub fn push(pc: &PathConfig, repo: &mut Repository) -> Result<()> {
let _commit = repo.find_commit(commit_oid)?; let _commit = repo.find_commit(commit_oid)?;
Ok(()) Ok(())
} }
pub fn pull(pc: &PathConfig, repo: &Repository) -> Result<()> {
validate_repo(&repo)?;
let mut remote = get_remote(&pc, &repo)?;
remote.fetch(&[&pc.config.remote.branch], None, None)?;
let remote_branch_name = format!("{}/{}", &pc.config.remote.name, &pc.config.remote.branch);
let remote_branch = repo.find_branch(&remote_branch_name, BranchType::Remote)?;
info!("Fetched remote branch `{}`.", remote_branch_name);
// There are two cases we need to consider:
//
// 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.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);
} else {
repo.branch_from_annotated_commit(&pc.config.remote.branch, &remote_ref, false)?;
info!("Created new local branch from `{}`.", remote_branch_name);
}
Ok(())
}
// ========================================
// Utility
// ========================================
/// Generate a new signature at the current time.
fn get_signature(pc: &PathConfig) -> Result<Signature> {
Ok(Signature::now(&pc.config.user.name, &pc.config.user.email)?)
}
/// Verify the repository we are working in supports the operations we want to
/// apply to it.
fn validate_repo(repo: &Repository) -> Result<()> {
repo.workdir().ok_or(Error::InvalidBareRepo)?;
Ok(())
}
/// Create or retrieve the remote specified within our configuration.
///
/// This method also configures the fetchspec for the remote, explicitly mapping
/// the remote branch against our local one.
///
/// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
fn get_remote<'repo>(pc: &PathConfig, repo: &'repo Repository) -> Result<Remote<'repo>> {
repo.remote_set_url(&pc.config.remote.name, &pc.config.remote.url.to_string())?;
repo.remote_add_fetch(
&pc.config.remote.name,
// We could go with "*" instead of {branch} for all remote branches.
&format!(
"+refs/heads/{branch}:refs/remotes/origin/{branch}",
branch = pc.config.remote.branch
),
)?;
Ok(repo.find_remote(&pc.config.remote.name)?)
}
/// Finds the latest commit relative to HEAD.
///
/// You should probably switch branches (refer to `switch_branch`) before
/// calling this function.
fn get_head_commit(repo: &Repository) -> Result<Option<Commit>> {
match repo.head() {
Ok(head) => {
let obj = head
.resolve()?
.peel(ObjectType::Commit)?
.into_commit()
.map_err(|_| git2::Error::from_str("Couldn't find commit"))?;
Ok(Some(obj))
}
Err(e) => match e.code() {
git2::ErrorCode::UnbornBranch => Ok(None),
_ => Err(e)?,
},
}
}

View File

@ -25,6 +25,12 @@ pub fn run_push(config: PathConfig) -> Result {
Ok(()) Ok(())
} }
pub fn run_pull(config: PathConfig) -> Result {
let repo = git::init(&config)?;
git::pull(&config, &repo)?;
Ok(())
}
pub fn run_stage(config: PathConfig) -> Result { pub fn run_stage(config: PathConfig) -> Result {
let repo = git::init(&config)?; let repo = git::init(&config)?;
git::stage(&config, &repo)?; git::stage(&config, &repo)?;

View File

@ -58,6 +58,7 @@ fn main() {
), ),
) )
.subcommand(App::new("list").about("See which packages homesync manages")) .subcommand(App::new("list").about("See which packages homesync manages"))
.subcommand(App::new("pull").about("Pull changes from remote to local"))
.subcommand(App::new("push").about("Push changes from local to remote")) .subcommand(App::new("push").about("Push changes from local to remote"))
.subcommand( .subcommand(
App::new("stage").about("Find all changes and stage them onto the local repository"), App::new("stage").about("Find all changes and stage them onto the local repository"),
@ -86,6 +87,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
Some(("list", _)) => Ok(homesync::run_list(config)?), Some(("list", _)) => Ok(homesync::run_list(config)?),
Some(("pull", _)) => Ok(homesync::run_pull(config)?),
Some(("push", _)) => Ok(homesync::run_push(config)?), Some(("push", _)) => Ok(homesync::run_push(config)?),
Some(("stage", _)) => Ok(homesync::run_stage(config)?), Some(("stage", _)) => Ok(homesync::run_stage(config)?),
_ => unreachable!(), _ => unreachable!(),