A component's build progress reads under its buttons, and an unrelated history can be overridden
Two things, both about a card being able to act on what it says. The progress bar, its counts and the last line a component printed now sit below that component's buttons rather than above them. A bar reports on the press that started it, so it reads in the order it happened -- and above, it pushed the buttons down the moment a build began, moving the row somebody had just pressed out from under their finger. A pull that cannot fast-forward because the checkout shares no history with its upstream now offers a way past it. There is no fast-forward between two unrelated histories and there never will be, so the card was one that could never be pulled again, with the only remedy on the build machine -- exactly where the person holding the phone isn't. The card reports the failure as before and a dialog offers a forced pull, naming the branch and the upstream it is about to overwrite; confirming sends ?force=true, which resets onto the upstream instead of merging. Whether it *is* that failure is decided structurally, by `git merge-base` finding no common ancestor, rather than by matching what git printed: those messages are translated, and a button that appeared only on an English build machine would be worse than no button. It travels to the phone as its own field for the same reason. The dirty-tree refusal stays in front of it, so a forced pull can only ever discard something that was committed, and a merely diverged history is not offered it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b0e83059a3
commit
db47972a25
6 files changed
+306
-29
No files matched your search
@@ -102,6 +102,12 @@ struct Inner {
|
||||
/// where this is being driven from and usually has no access to the
|
||||
/// server's log.
|
||||
error: Option<String>,
|
||||
/// Whether that failure was a pull with no fast-forward to make,
|
||||
/// which is the one the phone can offer a way past. Beside the
|
||||
/// message rather than inside it because the phone acts on it: a
|
||||
/// button that appears only when git happened to phrase itself a
|
||||
/// certain way is a button nobody can rely on.
|
||||
unrelated_histories: bool,
|
||||
/// The component whose step produced that failure.
|
||||
///
|
||||
/// Kept beside the message so the card can open the right log without
|
||||
@@ -180,6 +186,10 @@ pub struct BuildStatus {
|
||||
pub stale: bool,
|
||||
pub building: bool,
|
||||
pub error: Option<String>,
|
||||
/// That failure was a pull the checkout has no fast-forward for,
|
||||
/// because it shares no history with its upstream. The phone offers
|
||||
/// the forced pull for this and nothing else.
|
||||
pub unrelated_histories: bool,
|
||||
/// What the whole project is doing -- fetching, pulling -- absent
|
||||
/// between runs and while the work belongs to a component instead.
|
||||
pub phase: Option<String>,
|
||||
@@ -387,6 +397,7 @@ impl BuildState {
|
||||
inner.building = true;
|
||||
inner.error = None;
|
||||
inner.failed = None;
|
||||
inner.unrelated_histories = false;
|
||||
inner.started = Some(Instant::now());
|
||||
inner.runs.clear();
|
||||
}
|
||||
@@ -509,8 +520,14 @@ impl BuildState {
|
||||
/// whose new commits changed the declaration built anyway -- the card
|
||||
/// went off and updated the app instead of stopping to ask, which is
|
||||
/// the single case this gate exists for.
|
||||
/// `force` abandons this checkout's own history in favour of the
|
||||
/// upstream's, and is only ever pressed after a pull has already
|
||||
/// reported that the two are unrelated (`git::PullError`). It is a
|
||||
/// parameter rather than something decided here because it is a
|
||||
/// person's answer to that report, not a state of the repository.
|
||||
pub fn pull_and_build(
|
||||
self: &Arc<Self>,
|
||||
force: bool,
|
||||
may_build: impl Fn() -> bool + Send + 'static,
|
||||
record: RecordBuilt,
|
||||
) {
|
||||
@@ -522,18 +539,18 @@ impl BuildState {
|
||||
inner.building = true;
|
||||
inner.error = None;
|
||||
inner.failed = None;
|
||||
inner.unrelated_histories = false;
|
||||
inner.started = Some(Instant::now());
|
||||
inner.runs.clear();
|
||||
}
|
||||
|
||||
let this = Arc::clone(self);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let outcome = this.pull();
|
||||
let outcome = this.pull(force);
|
||||
match outcome {
|
||||
Err(message) => {
|
||||
tracing::error!("pull failed: {message}");
|
||||
// No component: a pull fails before the walk starts.
|
||||
this.finish(Some(message), None);
|
||||
Err(error) => {
|
||||
tracing::error!("pull failed: {}", error.message);
|
||||
this.fail_pull(error);
|
||||
}
|
||||
Ok(pulled) => {
|
||||
// Nothing configured to build, or nothing allowed to:
|
||||
@@ -553,7 +570,7 @@ impl BuildState {
|
||||
}
|
||||
|
||||
/// Fetches and fast-forwards, reporting whether anything arrived.
|
||||
fn pull(&self) -> Result<bool, String> {
|
||||
fn pull(&self, force: bool) -> Result<bool, crate::git::PullError> {
|
||||
self.begin_project("fetching");
|
||||
crate::git::fetch(&self.project_path, self.git_ipv4)?;
|
||||
// Counted after the fetch, from refs now on disk: this is the one
|
||||
@@ -563,7 +580,7 @@ impl BuildState {
|
||||
return Ok(false);
|
||||
}
|
||||
self.begin_project("pulling");
|
||||
crate::git::pull(&self.project_path, self.git_ipv4)?;
|
||||
crate::git::pull(&self.project_path, self.git_ipv4, force)?;
|
||||
tracing::info!(
|
||||
"pulled {behind} commit(s) into {}",
|
||||
self.project_path.display()
|
||||
@@ -990,6 +1007,19 @@ impl BuildState {
|
||||
inner.failed = failed;
|
||||
}
|
||||
|
||||
/// Finishes a run that never got past its pull.
|
||||
///
|
||||
/// Separate from [`Self::finish`] only because a pull failure carries
|
||||
/// the one thing a build failure cannot: whether abandoning this
|
||||
/// checkout's own history would clear it. Cleared where its siblings
|
||||
/// are, at the start of every run, so this is the only thing that can
|
||||
/// ever make it true.
|
||||
fn fail_pull(&self, error: crate::git::PullError) {
|
||||
self.inner.lock().unwrap().unrelated_histories = error.unrelated_histories;
|
||||
// No component: a pull fails before the walk starts.
|
||||
self.finish(Some(error.message), None);
|
||||
}
|
||||
|
||||
/// Records how far the running command says it has got. Replaces the
|
||||
/// previous count rather than accumulating: the command reports its
|
||||
/// own total, and `begin` clears this so one component's count is
|
||||
@@ -1041,6 +1071,7 @@ impl BuildState {
|
||||
stale,
|
||||
building: inner.building,
|
||||
error: inner.error.clone(),
|
||||
unrelated_histories: inner.unrelated_histories,
|
||||
phase: inner.phase.clone(),
|
||||
elapsed_ms: inner
|
||||
.started
|
||||
@@ -1230,6 +1261,7 @@ mod tests {
|
||||
|
||||
let project = clone.clone();
|
||||
state.pull_and_build(
|
||||
false,
|
||||
move || crate::config::project_config(&project).matches_accepted(&accepted, None),
|
||||
Arc::new(|_: &str, _: String| {}),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user