Add fetch & pull option to worktrees

This commit is contained in:
2021-12-29 11:19:00 +01:00
parent ef381c7421
commit 717b0d3a74
6 changed files with 385 additions and 22 deletions

View File

@@ -87,6 +87,10 @@ pub enum WorktreeAction {
Convert(WorktreeConvertArgs),
#[clap(about = "Clean all worktrees that do not contain uncommited/unpushed changes")]
Clean(WorktreeCleanArgs),
#[clap(about = "Fetch refs from remotes")]
Fetch(WorktreeFetchArgs),
#[clap(about = "Fetch refs from remotes and update local branches")]
Pull(WorktreePullArgs),
}
#[derive(Parser)]
@@ -121,6 +125,18 @@ pub struct WorktreeConvertArgs {}
#[derive(Parser)]
pub struct WorktreeCleanArgs {}
#[derive(Parser)]
pub struct WorktreeFetchArgs {}
#[derive(Parser)]
pub struct WorktreePullArgs {
#[clap(
long = "--rebase",
about = "Perform a rebase instead of a fast-forward"
)]
pub rebase: bool,
}
pub fn parse() -> Opts {
Opts::parse()
}

View File

@@ -310,6 +310,58 @@ fn main() {
));
}
}
cmd::WorktreeAction::Fetch(_args) => {
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
if error.kind == grm::RepoErrorKind::NotFound {
print_error("Directory does not contain a git repository");
} else {
print_error(&format!("Opening repository failed: {}", error));
}
process::exit(1);
});
repo.fetchall().unwrap_or_else(|error| {
print_error(&format!("Error fetching remotes: {}", error));
process::exit(1);
});
print_success("Fetched from all remotes");
}
cmd::WorktreeAction::Pull(args) => {
let repo = grm::Repo::open(&cwd, true).unwrap_or_else(|error| {
if error.kind == grm::RepoErrorKind::NotFound {
print_error("Directory does not contain a git repository");
} else {
print_error(&format!("Opening repository failed: {}", error));
}
process::exit(1);
});
repo.fetchall().unwrap_or_else(|error| {
print_error(&format!("Error fetching remotes: {}", error));
process::exit(1);
});
for worktree in repo.get_worktrees().unwrap_or_else(|error| {
print_error(&format!("Error getting worktrees: {}", error));
process::exit(1);
}) {
if let Some(warning) =
worktree
.forward_branch(args.rebase)
.unwrap_or_else(|error| {
print_error(&format!(
"Error updating worktree branch: {}",
error
));
process::exit(1);
})
{
print_warning(&format!("{}: {}", worktree.name(), warning));
} else {
print_success(&format!("{}: Done", worktree.name()));
}
}
}
}
}
}

View File

@@ -180,6 +180,77 @@ impl Worktree {
pub fn name(&self) -> &str {
&self.name
}
pub fn forward_branch(&self, rebase: bool) -> Result<Option<String>, String> {
let repo = Repo::open(Path::new(&self.name), false)
.map_err(|error| format!("Error opening worktree: {}", error))?;
if let Ok(remote_branch) = repo.find_local_branch(&self.name)?.upstream() {
let status = repo.status(false)?;
if !status.clean() {
return Ok(Some(String::from("Worktree contains changes")));
}
let remote_annotated_commit = repo
.0
.find_annotated_commit(remote_branch.commit()?.id().0)
.map_err(convert_libgit2_error)?;
if rebase {
let mut rebase = repo
.0
.rebase(
None, // use HEAD
Some(&remote_annotated_commit),
None, // figure out the base yourself, libgit2!
Some(&mut git2::RebaseOptions::new()),
)
.map_err(convert_libgit2_error)?;
while let Some(operation) = rebase.next() {
let operation = operation.map_err(convert_libgit2_error)?;
// This is required to preserve the commiter of the rebased
// commits, which is the expected behaviour.
let rebased_commit = repo
.0
.find_commit(operation.id())
.map_err(convert_libgit2_error)?;
let committer = rebased_commit.committer();
if rebase.commit(None, &committer, None).is_err() {
rebase.abort().map_err(convert_libgit2_error)?;
}
}
rebase.finish(None).map_err(convert_libgit2_error)?;
} else {
let (analysis, _preference) = repo
.0
.merge_analysis(&[&remote_annotated_commit])
.map_err(convert_libgit2_error)?;
if analysis.is_up_to_date() {
return Ok(None);
}
if !analysis.is_fast_forward() {
return Ok(Some(String::from("Worktree cannot be fast forwarded")));
}
repo.0
.reset(
remote_branch.commit()?.0.as_object(),
git2::ResetType::Hard,
Some(git2::build::CheckoutBuilder::new().safe()),
)
.map_err(convert_libgit2_error)?;
}
} else {
return Ok(Some(String::from("No remote branch to rebase onto")));
};
Ok(None)
}
}
#[cfg(test)]
@@ -364,6 +435,42 @@ impl Repo {
Ok(())
}
pub fn fetchall(&self) -> Result<(), String> {
for remote in self.remotes()? {
self.fetch(&remote)?;
}
Ok(())
}
pub fn local_branches(&self) -> Result<Vec<Branch>, String> {
self.0
.branches(Some(git2::BranchType::Local))
.map_err(convert_libgit2_error)?
.map(|branch| Ok(Branch(branch.map_err(convert_libgit2_error)?.0)))
.collect::<Result<Vec<Branch>, String>>()
}
pub fn fetch(&self, remote_name: &str) -> Result<(), String> {
let mut remote = self
.0
.find_remote(remote_name)
.map_err(convert_libgit2_error)?;
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(get_remote_callbacks());
for refspec in &remote.fetch_refspecs().map_err(convert_libgit2_error)? {
remote
.fetch(
&[refspec.ok_or("Remote name is invalid utf-8")?],
Some(&mut fetch_options),
None,
)
.map_err(convert_libgit2_error)?;
}
Ok(())
}
pub fn init(path: &Path, is_worktree: bool) -> Result<Self, String> {
let repo = match is_worktree {
false => Repository::init(path).map_err(convert_libgit2_error)?,
@@ -1090,6 +1197,37 @@ impl Branch<'_> {
}
}
fn get_remote_callbacks() -> git2::RemoteCallbacks<'static> {
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.push_update_reference(|_, status| {
if let Some(message) = status {
return Err(git2::Error::new(
git2::ErrorCode::GenericError,
git2::ErrorClass::None,
message,
));
}
Ok(())
});
callbacks.credentials(|_url, username_from_url, _allowed_types| {
let username = match username_from_url {
Some(username) => username,
None => panic!("Could not get username. This is a bug"),
};
git2::Cred::ssh_key_from_agent(username)
});
callbacks.credentials(|_url, username_from_url, _allowed_types| {
let username = match username_from_url {
Some(username) => username,
None => panic!("Could not get username. This is a bug"),
};
git2::Cred::ssh_key(username, None, &crate::env_home().join(".ssh/id_rsa"), None)
});
callbacks
}
impl RemoteHandle<'_> {
pub fn url(&self) -> String {
self.0
@@ -1121,29 +1259,8 @@ impl RemoteHandle<'_> {
return Err(String::from("Trying to push to a non-pushable remote"));
}
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.push_update_reference(|_, status| {
if let Some(message) = status {
return Err(git2::Error::new(
git2::ErrorCode::GenericError,
git2::ErrorClass::None,
message,
));
}
Ok(())
});
callbacks.credentials(|_url, username_from_url, _allowed_types| {
let username = match username_from_url {
Some(username) => username,
None => panic!("Could not get username. This is a bug"),
};
git2::Cred::ssh_key_from_agent(username).or_else(|_| {
git2::Cred::ssh_key(username, None, &crate::env_home().join(".ssh/id_rsa"), None)
})
});
let mut push_options = git2::PushOptions::new();
push_options.remote_callbacks(callbacks);
push_options.remote_callbacks(get_remote_callbacks());
let push_refspec = format!(
"+refs/heads/{}:refs/heads/{}",