Move a project's checkout to a branch or commit from the phone
The project settings sheet now lists the checkout's branches and its last fifty commits, and picking one moves the checkout on the build machine and builds what that leaves behind. It is the same act as a pull on the same single checkout, so it goes through the same machinery and reports in the same place: `after_moving` is the half the two share, which is not the git command but handing the run over under one lock, so nothing can observe the moment between the move ending and the builds it decided on starting. The two lists are read when the sheet opens rather than riding on the manifest, since both spawn git and the manifest is fetched on every open, resume and Refresh. Local reads only, so opening the sheet cannot stall on a network round trip: what the remote has and this checkout has not fetched is Pull's business, and the card already says when there is any. Listing branches was wrong twice in ways only a real checkout showed. `git branch --format` adds a `(HEAD detached at abc123)` pseudo-entry that is not a branch and cannot be checked out, so the list comes from `for-each-ref refs/heads`. And `refs/remotes/origin/HEAD` abbreviates to a bare `origin`, so filtering names that end in `/HEAD` matches nothing and a phantom branch called `origin` reached the phone; it is dropped by being a symref instead. The test written to cover the second asserted the same wrong thing and passed. Moving is refused over tracked modifications only, where a pull refuses over any dirt at all. The strict check makes this a one-way door: going back to a commit from before the .gitignore that covers this project's build output leaves that output untracked, so the tree is dirty and every move afterwards is refused -- back but never forward, from a phone, with the way out being the build machine. Nothing is lost by relaxing it, because git refuses to overwrite an untracked file itself and carries across the ones it would not, and its refusal arrives as the error the card already shows. Found by moving a checkout back and forth rather than by reading it: the first version passed its tests and trapped the checkout on the second move. Picking a commit leaves the checkout on no branch, which the card now says in those words rather than showing git's literal `HEAD` -- beside a branch icon that reads as a branch somebody named HEAD. Worth saying now that the sheet can produce the state, where before it was reachable only on the build machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
17d873ab7c
commit
3aa6f2f290
7 files changed
+841
-7
No files matched your search
@@ -694,6 +694,58 @@ impl BuildState {
|
||||
may_build: impl Fn() -> bool + Send + 'static,
|
||||
record: RecordBuilt,
|
||||
) {
|
||||
self.after_moving(move |this| this.pull(force), may_build, record);
|
||||
}
|
||||
|
||||
/// Moves this checkout onto `target` -- a branch or a commit -- and
|
||||
/// then builds whatever that left behind.
|
||||
///
|
||||
/// Exclusive with everything, and deferred to the same machinery as a
|
||||
/// pull, because it is the same kind of act: there is one checkout,
|
||||
/// and this rewrites the files every component builds from. It
|
||||
/// reports through the project's own state for the same reason, so a
|
||||
/// card shows it exactly as it shows a pull.
|
||||
pub fn checkout_and_build(
|
||||
self: &Arc<Self>,
|
||||
target: String,
|
||||
may_build: impl Fn() -> bool + Send + 'static,
|
||||
record: RecordBuilt,
|
||||
) {
|
||||
self.after_moving(
|
||||
move |this| {
|
||||
this.begin_project("checking out");
|
||||
crate::git::checkout(&this.project_path, &target)?;
|
||||
tracing::info!("checked out {target} in {}", this.project_path.display());
|
||||
// Always "something moved": a checkout onto the commit
|
||||
// that was already there is the one case this over-reports,
|
||||
// and the cost of that is one build that had nothing to do.
|
||||
// Under-reporting costs a checkout whose outputs stay from
|
||||
// the commit before it, which is the silent kind.
|
||||
Ok(true)
|
||||
},
|
||||
may_build,
|
||||
record,
|
||||
);
|
||||
}
|
||||
|
||||
/// The half a pull and a checkout share: run the thing that moves the
|
||||
/// working tree, then build what it left behind.
|
||||
///
|
||||
/// One copy, because the part that is easy to get wrong is not the
|
||||
/// git command -- it is handing the run over under a single lock so
|
||||
/// that nothing can observe the moment between the move ending and
|
||||
/// the builds it decided on starting. A phone polling in that gap
|
||||
/// sees a project that is neither pulling nor building and calls the
|
||||
/// run finished.
|
||||
fn after_moving(
|
||||
self: &Arc<Self>,
|
||||
move_it: impl FnOnce(&Arc<Self>) -> Result<bool, crate::git::PullError> + Send + 'static,
|
||||
may_build: impl Fn() -> bool + Send + 'static,
|
||||
record: RecordBuilt,
|
||||
) {
|
||||
// Claimed before anything is spawned, and while nothing else is
|
||||
// running: one checkout, so a move is exclusive with every build
|
||||
// as well as with another move.
|
||||
{
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
if inner.anything_running() {
|
||||
@@ -703,13 +755,12 @@ impl BuildState {
|
||||
inner.error = None;
|
||||
inner.unrelated_histories = false;
|
||||
}
|
||||
|
||||
let this = Arc::clone(self);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let outcome = this.pull(force);
|
||||
let outcome = move_it(&this);
|
||||
match outcome {
|
||||
Err(error) => {
|
||||
tracing::error!("pull failed: {}", error.message);
|
||||
tracing::error!("moving the checkout failed: {}", error.message);
|
||||
this.fail_pull(error);
|
||||
}
|
||||
Ok(pulled) => {
|
||||
|
||||
Reference in new issue
Block a user