Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create git submit command #541

Merged
merged 2 commits into from
Sep 17, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions git-branchless-lib/src/core/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub enum OperationType {
InitializeRebase,
MakeGraph,
ProcessEvents,
PushBranches,
QueryWorkingCopy,
ReadingFromCache,
RebaseCommits,
Expand Down Expand Up @@ -66,6 +67,7 @@ impl ToString for OperationType {
OperationType::GetUpstreamPatchIds => "Enumerating patch IDs",
OperationType::InitializeRebase => "Initializing rebase",
OperationType::MakeGraph => "Examining local history",
OperationType::PushBranches => "Pushing branches",
OperationType::ProcessEvents => "Processing events",
OperationType::QueryWorkingCopy => "Querying the working copy",
OperationType::ReadingFromCache => "Reading from cache",
Expand Down
67 changes: 64 additions & 3 deletions git-branchless-lib/src/git/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ pub enum Error {
#[error("could not get branches: {0}")]
GetBranches(#[source] git2::Error),

#[error("could not get remote names: {0}")]
GetRemoteNames(#[source] git2::Error),

#[error("HEAD is unborn (try making a commit?)")]
UnbornHead,

Expand Down Expand Up @@ -943,6 +946,24 @@ impl Repo {
Ok(Reference { inner: reference })
}

/// Get a list of all remote names.
#[instrument]
pub fn get_all_remote_names(&self) -> Result<Vec<String>> {
let remotes = self.inner.remotes().map_err(Error::GetRemoteNames)?;
Ok(remotes
.into_iter()
.enumerate()
.filter_map(|(i, remote_name)| match remote_name {
Some(remote_name) => Some(remote_name.to_owned()),
None => {
warn!(remote_index = i, "Remote name could not be decoded");
None
}
})
.sorted()
.collect())
}

/// Look up a reference with the given name. Returns `None` if not found.
#[instrument]
pub fn find_reference(&self, name: &ReferenceName) -> Result<Option<Reference>> {
Expand All @@ -966,7 +987,10 @@ impl Repo {
.map_err(Error::GetBranches)?
{
let (branch, _branch_type) = branch.map_err(Error::ReadBranch)?;
all_branches.push(Branch { inner: branch });
all_branches.push(Branch {
repo: self,
inner: branch,
});
}
Ok(all_branches)
}
Expand All @@ -975,7 +999,10 @@ impl Repo {
#[instrument]
pub fn find_branch(&self, name: &str, branch_type: BranchType) -> Result<Option<Branch>> {
match self.inner.find_branch(name, branch_type) {
Ok(branch) => Ok(Some(Branch { inner: branch })),
Ok(branch) => Ok(Some(Branch {
repo: self,
inner: branch,
})),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => Err(Error::FindBranch {
source: err,
Expand Down Expand Up @@ -1919,6 +1946,9 @@ impl<'repo> Reference<'repo> {
/// `suffix` value is converted to a `String` to be rendered to the screen, so
/// it may have lost some information if the reference name had unusual
/// characters.
///
/// FIXME: This abstraction seems uncomfortable and clunky to use; consider
/// revising.
#[derive(Debug)]
pub enum CategorizedReferenceName<'a> {
/// The reference represents a local branch.
Expand Down Expand Up @@ -2044,6 +2074,7 @@ impl Time {

/// Represents a Git branch.
pub struct Branch<'repo> {
repo: &'repo Repo,
inner: git2::Branch<'repo>,
}

Expand All @@ -2068,11 +2099,22 @@ impl<'repo> Branch<'repo> {
Ok(self.inner.get().target().map(make_non_zero_oid))
}

/// Get the name of this branch, not including any `refs/heads/` prefix.
#[instrument]
pub fn get_name(&self) -> eyre::Result<&str> {
self.inner
.name()?
.ok_or_else(|| eyre::eyre!("Could not decode branch name"))
}

/// If this branch tracks a remote ("upstream") branch, return that branch.
#[instrument]
pub fn get_upstream_branch(&self) -> Result<Option<Branch<'repo>>> {
match self.inner.upstream() {
Ok(upstream) => Ok(Some(Branch { inner: upstream })),
Ok(upstream) => Ok(Some(Branch {
repo: self.repo,
inner: upstream,
})),
Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
Err(err) => {
let branch_name = self.inner.name_bytes().map_err(|_err| Error::DecodeUtf8 {
Expand All @@ -2086,6 +2128,25 @@ impl<'repo> Branch<'repo> {
}
}

/// Get the associated remote to push to for this branch. If there is no
/// associated remote, returns `None`. Note that this never reads the value
/// of `push.remoteDefault`.
#[instrument]
pub fn get_push_remote_name(&self) -> eyre::Result<Option<String>> {
let branch_name = self
.inner
.name()?
.ok_or_else(|| eyre::eyre!("Branch name was not UTF-8: {self:?}"))?;
let config = self.repo.get_readonly_config()?;
if let Some(remote_name) = config.get(format!("branch.{branch_name}.pushRemote"))? {
Ok(Some(remote_name))
} else if let Some(remote_name) = config.get(format!("branch.{branch_name}.remote"))? {
Ok(Some(remote_name))
} else {
Ok(None)
}
}

/// Convert the branch into its underlying `Reference`.
pub fn into_reference(self) -> Reference<'repo> {
Reference {
Expand Down
3 changes: 2 additions & 1 deletion git-branchless-lib/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,14 @@ stderr:
/// Clone this repository into the `target` repository (which must not have
/// been initialized).
pub fn clone_repo_into(&self, target: &Git, additional_args: &[&str]) -> eyre::Result<()> {
let remote = format!("file://{}", self.repo_path.to_str().unwrap());
let args = {
let mut args = vec![
"clone",
// For Windows in CI.
"-c",
"core.autocrlf=false",
self.repo_path.to_str().unwrap(),
&remote,
target.repo_path.to_str().unwrap(),
];
args.extend(additional_args.iter());
Expand Down
1 change: 1 addition & 0 deletions git-branchless/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const ALL_ALIASES: &[(&str, &str)] = &[
("reword", "reword"),
("sl", "smartlog"),
("smartlog", "smartlog"),
("submit", "submit"),
("sync", "sync"),
("undo", "undo"),
("unhide", "unhide"),
Expand Down
5 changes: 5 additions & 0 deletions git-branchless/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod restack;
mod reword;
mod smartlog;
mod snapshot;
mod submit;
mod sync;
mod undo;
mod wrap;
Expand Down Expand Up @@ -310,6 +311,10 @@ fn do_main_and_drop_locals() -> eyre::Result<i32> {
}
},

Command::Submit { create, revset } => {
submit::submit(&effects, &git_run_info, revset, create)?
}

Command::Sync {
update_refs,
move_options,
Expand Down
211 changes: 211 additions & 0 deletions git-branchless/src/commands/submit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
use std::fmt::Write;
use std::time::SystemTime;

use itertools::{Either, Itertools};
use lib::core::dag::{commit_set_to_vec_unsorted, Dag};
use lib::core::effects::{Effects, OperationType};
use lib::core::eventlog::{EventLogDb, EventReplayer};
use lib::core::formatting::Pluralize;
use lib::core::repo_ext::RepoExt;
use lib::git::{Branch, BranchType, CategorizedReferenceName, ConfigRead, GitRunInfo, Repo};
use lib::util::ExitCode;

use crate::opts::Revset;
use crate::revset::resolve_commits;

pub fn submit(
effects: &Effects,
git_run_info: &GitRunInfo,
revset: Revset,
create: bool,
) -> eyre::Result<ExitCode> {
let repo = Repo::from_current_dir()?;
let conn = repo.get_db_conn()?;
let event_log_db = EventLogDb::new(&conn)?;
let now = SystemTime::now();
let event_tx_id = event_log_db.make_transaction_id(now, "submit")?;
let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
let event_cursor = event_replayer.make_default_cursor();
let references_snapshot = repo.get_references_snapshot()?;
let mut dag = Dag::open_and_sync(
effects,
&repo,
&event_replayer,
event_cursor,
&references_snapshot,
)?;

let commit_set = match resolve_commits(effects, &repo, &mut dag, vec![revset]) {
Ok(mut commit_sets) => commit_sets.pop().unwrap(),
Err(err) => {
err.describe(effects)?;
return Ok(ExitCode(1));
}
};

let branches: Vec<Branch> = commit_set_to_vec_unsorted(&commit_set)?
.into_iter()
.flat_map(|commit_oid| references_snapshot.branch_oid_to_names.get(&commit_oid))
.flatten()
.filter_map(
|reference_name| match CategorizedReferenceName::new(reference_name) {
name @ CategorizedReferenceName::LocalBranch { .. } => name.remove_prefix().ok(),
CategorizedReferenceName::RemoteBranch { .. }
| CategorizedReferenceName::OtherRef { .. } => None,
},
)
.map(|branch_name| -> eyre::Result<Branch> {
let branch = repo.find_branch(&branch_name, BranchType::Local)?;
let branch =
branch.ok_or_else(|| eyre::eyre!("Could not look up branch {branch_name:?}"))?;
Ok(branch)
})
.collect::<Result<_, _>>()?;
let branches_and_remotes: Vec<(Branch, Option<String>)> = branches
.into_iter()
.map(|branch| -> eyre::Result<_> {
let remote_name = branch.get_push_remote_name()?;
Ok((branch, remote_name))
})
.collect::<Result<_, _>>()?;
let (branches_without_remotes, branches_with_remotes): (Vec<_>, Vec<_>) = branches_and_remotes
.into_iter()
.partition_map(|(branch, remote_name)| match remote_name {
None => Either::Left(branch),
Some(remote_name) => Either::Right((branch, remote_name)),
});
let remotes_to_branches = branches_with_remotes
.into_iter()
.map(|(v, k)| (k, v))
.into_group_map();

let total_num_pushed_branches = {
let (effects, progress) = effects.start_operation(OperationType::PushBranches);
let total_num_branches = remotes_to_branches
.values()
.map(|branches| branches.len())
.sum();
progress.notify_progress(0, total_num_branches);
for (remote_name, branches) in remotes_to_branches
.iter()
.sorted_by(|(k1, _v1), (k2, _v2)| k1.cmp(k2))
{
let mut branch_names: Vec<&str> = branches
.iter()
.map(|branch| branch.get_name())
.collect::<Result<_, _>>()?;
branch_names.sort_unstable();
let mut args = vec!["push", "--force-with-lease", remote_name];
args.extend(branch_names.iter());
let exit_code = git_run_info.run(&effects, Some(event_tx_id), &args)?;
if !exit_code.is_success() {
writeln!(
effects.get_output_stream(),
"Failed to push branches: {}",
branch_names.into_iter().join(", ")
)?;
return Ok(exit_code);
}
progress.notify_progress_inc(branches.len());
}
total_num_branches
};

let total_num_pushed_branches = total_num_pushed_branches + {
let mut branch_names: Vec<&str> = branches_without_remotes
.iter()
.map(|branch| branch.get_name())
.collect::<Result<_, _>>()?;
branch_names.sort_unstable();
if create {
let push_remote: String = match get_default_remote(&repo)? {
Some(push_remote) => push_remote,
None => {
writeln!(
effects.get_output_stream(),
"\
No upstream repository was associated with {} and no value was
specified for `remote.pushDefault`, so cannot push these branches: {}
Configure a value with: git config remote.pushDefault <remote>
These remotes are available: {}",
CategorizedReferenceName::new(
&repo.get_main_branch_reference()?.get_name()?
)
.friendly_describe(),
branch_names.join(", "),
repo.get_all_remote_names()?.join(", "),
)?;
return Ok(ExitCode(1));
}
};
let mut args = vec!["push", "--force-with-lease", "--set-upstream", &push_remote];
args.extend(branch_names.iter());
{
let (effects, progress) = effects.start_operation(OperationType::PushBranches);
progress.notify_progress(0, branch_names.len());
let exit_code = git_run_info.run(&effects, Some(event_tx_id), &args)?;
if !exit_code.is_success() {
return Ok(exit_code);
}
}
branch_names.len()
} else {
if !branches_without_remotes.is_empty() {
writeln!(
effects.get_output_stream(),
"\
Skipped pushing these branches because they were not already associated with a
remote repository: {}",
branch_names.join(", ")
)?;
writeln!(
effects.get_output_stream(),
"\
To create and push them, retry this operation with the --create option."
)?;
}
0
}
};

writeln!(
effects.get_output_stream(),
"Successfully pushed {}.",
Pluralize {
determiner: None,
amount: total_num_pushed_branches,
unit: ("branch", "branches")
}
)?;
Ok(ExitCode(0))
}

fn get_default_remote(repo: &Repo) -> eyre::Result<Option<String>> {
let main_branch_reference = repo.get_main_branch_reference()?;
let main_branch_name = main_branch_reference.get_name()?;
match CategorizedReferenceName::new(&main_branch_name) {
name @ CategorizedReferenceName::LocalBranch { .. } => {
if let Some(main_branch) =
repo.find_branch(&name.remove_prefix()?, BranchType::Local)?
{
if let Some(remote_name) = main_branch.get_push_remote_name()? {
return Ok(Some(remote_name));
}
}
}

name @ CategorizedReferenceName::RemoteBranch { .. } => {
let name = name.remove_prefix()?;
if let Some((remote_name, _reference_name)) = name.split_once('/') {
return Ok(Some(remote_name.to_owned()));
}
}

CategorizedReferenceName::OtherRef { .. } => {
// Do nothing.
}
}

let push_default_remote_opt = repo.get_readonly_config()?.get("remote.pushDefault")?;
Ok(push_default_remote_opt)
}
Loading