Skip to content

Commit da5e8f5

Browse files
committed
feat(submit:phabricator): do not abort entire process on failure
Currently, `git submit` for Phabricator will abort the entire operation if any commit fails to be submitted. This means that if `arc diff` succeeds on one commit and then fails on its child, the entire operation is aborted. However, the first `arc diff` had side effects, so the user gets diffs uploaded to Phabricator that are not reflected locally. Instead, we should confirm any passing commits and abort after we get a failing commit. This commit updates the Phabricator forge to handle the error case better and not produce garbage commits on Phabricator.
1 parent 6056d2d commit da5e8f5

File tree

5 files changed

+244
-82
lines changed

5 files changed

+244
-82
lines changed

git-branchless-lib/src/core/eventlog.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,6 @@ INSERT INTO event_log VALUES (
580580
///
581581
/// Returns: All the events in the database, ordered from oldest to newest.
582582
#[instrument]
583-
584583
pub fn get_events(&self) -> eyre::Result<Vec<Event>> {
585584
let mut stmt = self.conn.prepare(
586585
"

git-branchless-submit/src/branch_forge.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ These remotes are available: {}",
247247
commit_status.local_branch_name.map(|local_branch_name| {
248248
(
249249
commit_oid,
250-
CreateStatus {
250+
CreateStatus::Created {
251251
final_commit_oid: commit_oid,
252252
local_branch_name,
253253
},

git-branchless-submit/src/lib.rs

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,23 @@ pub struct SubmitOptions {
109109

110110
/// The result of creating a commit.
111111
#[derive(Clone, Debug)]
112-
pub struct CreateStatus {
113-
/// The commit OID after carrying out the creation process. Usually, this
114-
/// will be the same as the original commit OID, unless the forge amends it
115-
/// (e.g. to include a change ID).
116-
pub final_commit_oid: NonZeroOid,
117-
118-
/// The local branch name to use. The caller will try to create the branch
119-
/// pointing to that commit (assuming that it doesn't already exist).
120-
pub local_branch_name: String,
112+
pub enum CreateStatus {
113+
Created {
114+
/// The commit OID after carrying out the creation process. Usually, this
115+
/// will be the same as the original commit OID, unless the forge amends it
116+
/// (e.g. to include a change ID).
117+
final_commit_oid: NonZeroOid,
118+
119+
/// The local branch name to use. The caller will try to create the branch
120+
/// pointing to that commit (assuming that it doesn't already exist).
121+
local_branch_name: String,
122+
},
123+
Skipped {
124+
commit_oid: NonZeroOid,
125+
},
126+
Err {
127+
commit_oid: NonZeroOid,
128+
},
121129
}
122130

123131
/// "Forge" refers to a Git hosting provider, such as GitHub, GitLab, etc.
@@ -325,15 +333,19 @@ fn submit(
325333
(unsubmitted, to_update, to_skip)
326334
});
327335

328-
let (created_branches, uncreated_branches): (BTreeSet<String>, BTreeSet<String>) = {
336+
let (created_branches, uncreated_branches, create_error_commits): (
337+
BTreeSet<String>,
338+
BTreeSet<String>,
339+
BTreeSet<NonZeroOid>,
340+
) = {
329341
let unsubmitted_branches = unsubmitted_commits
330342
.values()
331343
.flat_map(|commit_status| commit_status.local_branch_name.clone())
332344
.collect();
333345
if unsubmitted_commits.is_empty() {
334346
Default::default()
335347
} else if create && dry_run {
336-
(unsubmitted_branches, Default::default())
348+
(unsubmitted_branches, Default::default(), Default::default())
337349
} else if create {
338350
let create_statuses =
339351
try_exit_code!(forge.create(unsubmitted_commits, &submit_options)?);
@@ -343,26 +355,37 @@ fn submit(
343355
.flatten()
344356
.collect();
345357
let mut created_branches = BTreeSet::new();
358+
let mut error_commits = BTreeSet::new();
346359
for (_commit_oid, create_status) in create_statuses {
347-
let CreateStatus {
348-
final_commit_oid,
349-
local_branch_name,
350-
} = create_status;
351-
let branch_reference_name =
352-
ReferenceName::from(format!("refs/heads/{local_branch_name}"));
353-
created_branches.insert(local_branch_name);
354-
if !all_branches.contains(&branch_reference_name) {
355-
repo.create_reference(
356-
&branch_reference_name,
360+
match create_status {
361+
CreateStatus::Created {
357362
final_commit_oid,
358-
false,
359-
"submit",
360-
)?;
363+
local_branch_name,
364+
} => {
365+
let branch_reference_name =
366+
ReferenceName::from(format!("refs/heads/{local_branch_name}"));
367+
created_branches.insert(local_branch_name);
368+
if !all_branches.contains(&branch_reference_name) {
369+
repo.create_reference(
370+
&branch_reference_name,
371+
final_commit_oid,
372+
false,
373+
"submit",
374+
)?;
375+
}
376+
}
377+
// For now, treat `Skipped` the same as `Err` as it would be
378+
// a lot of work to render it differently in the output, and
379+
// we may want to rethink the data structures before doing
380+
// that.
381+
CreateStatus::Skipped { commit_oid } | CreateStatus::Err { commit_oid } => {
382+
error_commits.insert(commit_oid);
383+
}
361384
}
362385
}
363-
(created_branches, Default::default())
386+
(created_branches, Default::default(), error_commits)
364387
} else {
365-
(Default::default(), unsubmitted_branches)
388+
(Default::default(), unsubmitted_branches, Default::default())
366389
}
367390
};
368391

@@ -482,8 +505,33 @@ create and push them, retry this operation with the --create option.",
482505
if dry_run { "are" } else { "were" },
483506
)?;
484507
}
508+
if !create_error_commits.is_empty() {
509+
writeln!(
510+
effects.get_output_stream(),
511+
"Failed to create {}:",
512+
Pluralize {
513+
determiner: None,
514+
amount: create_error_commits.len(),
515+
unit: ("commit", "commits")
516+
},
517+
)?;
518+
for error_commit_oid in &create_error_commits {
519+
let error_commit = repo.find_commit_or_fail(*error_commit_oid)?;
520+
writeln!(
521+
effects.get_output_stream(),
522+
"{}",
523+
effects
524+
.get_glyphs()
525+
.render(error_commit.friendly_describe(effects.get_glyphs())?)?
526+
)?;
527+
}
528+
}
485529

486-
Ok(Ok(()))
530+
if !create_error_commits.is_empty() {
531+
Ok(Err(ExitCode(1)))
532+
} else {
533+
Ok(Ok(()))
534+
}
487535
}
488536

489537
#[instrument]

git-branchless-submit/src/phabricator.rs

Lines changed: 87 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::time::SystemTime;
1010

1111
use cursive_core::theme::Effect;
1212
use cursive_core::utils::markup::StyledString;
13-
use git_branchless_opts::Revset;
13+
use git_branchless_opts::{Revset, TestSearchStrategy};
1414
use git_branchless_test::{
1515
run_tests, FixInfo, ResolvedTestOptions, TestOutput, TestResults, TestStatus,
1616
TestingAbortedError, Verbosity,
@@ -373,7 +373,7 @@ impl Forge for PhabricatorForge<'_> {
373373
TestCommand::Args(args.into_iter().map(ToString::to_string).collect())
374374
} else {
375375
TestCommand::String(
376-
r#"git commit --amend --message "$(git show --no-patch --format=%B HEAD)
376+
r#"(git show | grep 'BROKEN') && exit 1 || git commit --amend --message "$(git show --no-patch --format=%B HEAD)
377377
378378
Differential Revision: https://phabricator.example.com/D000$(git rev-list --count HEAD)
379379
"
@@ -394,7 +394,7 @@ Differential Revision: https://phabricator.example.com/D000$(git rev-list --coun
394394
&ResolvedTestOptions {
395395
command,
396396
execution_strategy: *execution_strategy,
397-
search_strategy: None,
397+
search_strategy: Some(TestSearchStrategy::Linear),
398398
is_dry_run: false,
399399
use_cache: false,
400400
is_interactive: false,
@@ -429,10 +429,23 @@ Differential Revision: https://phabricator.example.com/D000$(git rev-list --coun
429429
return Ok(Err(ExitCode(1)));
430430
}
431431

432-
let rebase_plan = {
432+
enum ErrorReason {
433+
NotTested,
434+
TestFailed,
435+
}
436+
let (maybe_rebase_plan, error_commits) = {
433437
let mut builder = RebasePlanBuilder::new(self.dag, permissions);
434-
for (commit_oid, test_output) in test_outputs {
435-
let head_commit_oid = match test_output.test_status {
438+
let mut error_commits = HashMap::new();
439+
for commit_oid in commit_oids.iter().copied() {
440+
let test_output = match test_outputs.get(&commit_oid) {
441+
Some(test_output) => test_output,
442+
None => {
443+
// Testing was aborted due to a previous commit failing.
444+
error_commits.insert(commit_oid, ErrorReason::NotTested);
445+
continue;
446+
}
447+
};
448+
match test_output.test_status {
436449
TestStatus::CheckoutFailed
437450
| TestStatus::SpawnTestFailed(_)
438451
| TestStatus::TerminatedBySignal
@@ -442,7 +455,7 @@ Differential Revision: https://phabricator.example.com/D000$(git rev-list --coun
442455
| TestStatus::Abort { .. }
443456
| TestStatus::Failed { .. } => {
444457
self.render_failed_test(commit_oid, &test_output)?;
445-
return Ok(Err(ExitCode(1)));
458+
error_commits.insert(commit_oid, ErrorReason::TestFailed);
446459
}
447460
TestStatus::Passed {
448461
cached: _,
@@ -452,66 +465,86 @@ Differential Revision: https://phabricator.example.com/D000$(git rev-list --coun
452465
snapshot_tree_oid: _,
453466
},
454467
interactive: _,
455-
} => head_commit_oid,
456-
};
457-
458-
let commit = self.repo.find_commit_or_fail(commit_oid)?;
459-
builder.move_subtree(commit.get_oid(), commit.get_parent_oids())?;
460-
builder.replace_commit(commit.get_oid(), head_commit_oid.unwrap_or(commit_oid))?;
468+
} => {
469+
let commit = self.repo.find_commit_or_fail(commit_oid)?;
470+
builder.move_subtree(commit.get_oid(), commit.get_parent_oids())?;
471+
builder.replace_commit(
472+
commit.get_oid(),
473+
head_commit_oid.unwrap_or(commit_oid),
474+
)?;
475+
}
476+
}
461477
}
462478

463479
let pool = ThreadPoolBuilder::new().build()?;
464480
let repo_pool = RepoResource::new_pool(self.repo)?;
465-
match builder.build(self.effects, &pool, &repo_pool)? {
466-
Ok(Some(rebase_plan)) => rebase_plan,
467-
Ok(None) => return Ok(Ok(Default::default())),
481+
let maybe_rebase_plan = match builder.build(self.effects, &pool, &repo_pool)? {
482+
Ok(maybe_rebase_plan) => maybe_rebase_plan,
468483
Err(err) => {
469484
err.describe(self.effects, self.repo, self.dag)?;
470485
return Ok(Err(ExitCode(1)));
471486
}
472-
}
487+
};
488+
(maybe_rebase_plan, error_commits)
473489
};
474490

475-
let rewritten_oids = match execute_rebase_plan(
476-
self.effects,
477-
self.git_run_info,
478-
self.repo,
479-
self.event_log_db,
480-
&rebase_plan,
481-
&execute_options,
482-
)? {
483-
ExecuteRebasePlanResult::Succeeded {
484-
rewritten_oids: Some(rewritten_oids),
485-
} => rewritten_oids,
486-
ExecuteRebasePlanResult::Succeeded {
487-
rewritten_oids: None,
488-
} => {
489-
warn!("No rewritten commit OIDs were produced by rebase plan execution");
490-
Default::default()
491-
}
492-
ExecuteRebasePlanResult::DeclinedToMerge {
493-
failed_merge_info: _,
494-
} => {
495-
writeln!(
496-
self.effects.get_error_stream(),
497-
"BUG: Merge failed, but rewording shouldn't cause any merge failures."
498-
)?;
499-
return Ok(Err(ExitCode(1)));
500-
}
501-
ExecuteRebasePlanResult::Failed { exit_code } => {
502-
return Ok(Err(exit_code));
503-
}
491+
let rewritten_oids = match maybe_rebase_plan {
492+
None => Default::default(),
493+
Some(rebase_plan) => match execute_rebase_plan(
494+
self.effects,
495+
self.git_run_info,
496+
self.repo,
497+
self.event_log_db,
498+
&rebase_plan,
499+
&execute_options,
500+
)? {
501+
ExecuteRebasePlanResult::Succeeded {
502+
rewritten_oids: Some(rewritten_oids),
503+
} => rewritten_oids,
504+
ExecuteRebasePlanResult::Succeeded {
505+
rewritten_oids: None,
506+
} => {
507+
warn!("No rewritten commit OIDs were produced by rebase plan execution");
508+
Default::default()
509+
}
510+
ExecuteRebasePlanResult::DeclinedToMerge {
511+
failed_merge_info: _,
512+
} => {
513+
writeln!(
514+
self.effects.get_error_stream(),
515+
"BUG: Merge failed, but rewording shouldn't cause any merge failures."
516+
)?;
517+
return Ok(Err(ExitCode(1)));
518+
}
519+
ExecuteRebasePlanResult::Failed { exit_code } => {
520+
return Ok(Err(exit_code));
521+
}
522+
},
504523
};
505524

506525
let mut create_statuses = HashMap::new();
507526
for commit_oid in commit_oids {
527+
if let Some(error_reason) = error_commits.get(&commit_oid) {
528+
create_statuses.insert(
529+
commit_oid,
530+
match error_reason {
531+
ErrorReason::NotTested => CreateStatus::Skipped { commit_oid },
532+
ErrorReason::TestFailed => CreateStatus::Err { commit_oid },
533+
},
534+
);
535+
continue;
536+
}
537+
508538
let final_commit_oid = match rewritten_oids.get(&commit_oid) {
509539
Some(MaybeZeroOid::NonZero(commit_oid)) => *commit_oid,
510540
Some(MaybeZeroOid::Zero) => {
511541
warn!(?commit_oid, "Commit was rewritten to the zero OID",);
512542
commit_oid
513543
}
514-
None => commit_oid,
544+
None => {
545+
create_statuses.insert(commit_oid, CreateStatus::Skipped { commit_oid });
546+
continue;
547+
}
515548
};
516549
let local_branch_name = {
517550
match self.get_revision_id(final_commit_oid)? {
@@ -527,13 +560,14 @@ Differential Revision: https://phabricator.example.com/D000$(git rev-list --coun
527560
)?
528561
)?,
529562
)?;
530-
return Ok(Err(ExitCode(1)));
563+
create_statuses.insert(commit_oid, CreateStatus::Err { commit_oid });
564+
continue;
531565
}
532566
}
533567
};
534568
create_statuses.insert(
535569
commit_oid,
536-
CreateStatus {
570+
CreateStatus::Created {
537571
final_commit_oid,
538572
local_branch_name,
539573
},
@@ -542,12 +576,12 @@ Differential Revision: https://phabricator.example.com/D000$(git rev-list --coun
542576

543577
let final_commit_oids: CommitSet = create_statuses
544578
.values()
545-
.map(|create_status| {
546-
let CreateStatus {
579+
.filter_map(|create_status| match create_status {
580+
CreateStatus::Created {
547581
final_commit_oid,
548582
local_branch_name: _,
549-
} = create_status;
550-
*final_commit_oid
583+
} => Some(*final_commit_oid),
584+
CreateStatus::Skipped { .. } | CreateStatus::Err { .. } => None,
551585
})
552586
.collect();
553587
self.dag.sync_from_oids(

0 commit comments

Comments
 (0)