Fetch is a button, and moving the checkout builds nothing
Nothing here ever wrote a remote-tracking ref except the fetch inside a
pull, and a pull is reachable only when the branch you are already on is
behind -- so a branch pushed from another machine reached the picker as a
side effect of pulling something else, and a project already up to date
could never be moved onto a new branch at all.
POST /apps/{key}/fetch answers with the whole ref list, so the pickers
repopulate from after the fetch in one round trip. It is the only call
here that asks for --prune: making the picker say what the remote says is
its job, where a pull should change as little as it can. Still not on a
timer and not on opening the sheet, because a fetch mutates the checkout.
Picking a remote-only branch then had to work: `git checkout origin/topic`
detaches HEAD, since git's DWIM fires on the bare name, so the control
that says it is picking a branch produced the state picking a commit is
meant to produce -- and printed its detached-HEAD advice and succeeded.
Moving the checkout no longer builds. A pull is somebody taking new work;
a move is somebody looking, and charging a full build for a look starts a
minute of work an accidental tap cannot call back. What it leaves behind
is a component whose build no longer matches the checkout, so the build
button's word now follows the state: Rebuild only where every component
it covers is known current, Build otherwise.
The sheet loses its Checkout heading and its paragraph, both pickers sit
in a weighted row so a long branch name cannot wrap the label one letter
per line, and the failure text moved below them -- above, it shoved the
pickers down the screen as somebody reached for one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f983db154d
commit
25c069909b
8 files changed
+507
-117
No files matched your search
@@ -38,6 +38,7 @@ import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
@@ -895,11 +896,13 @@ private fun AppListScreen(
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves this project's checkout onto a branch or a commit on the build machine, and builds what
|
||||
* that leaves behind.
|
||||
* Moves this project's checkout onto a branch or a commit on the build machine, and builds
|
||||
* nothing.
|
||||
*
|
||||
* Followed exactly as a pull is, because it is the same act on the same single checkout: one
|
||||
* progress path, reported in the project's own row, rather than a second one to keep in step.
|
||||
* It is over as soon as git is, though, since there is no build behind it -- what the move
|
||||
* leaves behind is a component the card reports as out of date, with its own Build beside it.
|
||||
*/
|
||||
fun startCheckout(entry: ManifestEntry, target: String) {
|
||||
scope.launch {
|
||||
@@ -2035,7 +2038,12 @@ private fun AppCard(
|
||||
// behind is not obvious from here.
|
||||
colors = ActionTone.Caution.colors(),
|
||||
) {
|
||||
Text("Rebuild")
|
||||
// Asked of the components this actually builds:
|
||||
// one with no command of its own has no output to
|
||||
// be current with anything, and counting it would
|
||||
// leave every such project's button saying Build
|
||||
// for ever.
|
||||
Text(buildWord(entry.components.filter { it.hasBuild }))
|
||||
}
|
||||
}
|
||||
if (awaitingApproval) {
|
||||
@@ -2226,6 +2234,12 @@ private fun ProjectSettingsDialog(
|
||||
* that does nothing: not a checkout at all, something already running on it, uncommitted work in
|
||||
* the way, still reading, or the read failed. Those are five different things to do next, and a
|
||||
* disabled dropdown with no sentence beside it is the same picture for all of them.
|
||||
*
|
||||
* Fetch is the third control and the only one that touches the network. It is what puts a branch
|
||||
* pushed from another machine into the picker above it -- without it the list is whatever the last
|
||||
* pull happened to bring in, so a project already up to date on its own branch could never be moved
|
||||
* onto a new one. It stays a press rather than something the sheet does on opening, because a fetch
|
||||
* writes into the checkout and pulls down objects: opening a sheet should not do either.
|
||||
*/
|
||||
@Composable
|
||||
private fun CheckoutSection(
|
||||
@@ -2235,6 +2249,8 @@ private fun CheckoutSection(
|
||||
) {
|
||||
var refs by remember(entry.key) { mutableStateOf<CheckoutRefs?>(null) }
|
||||
var failure by remember(entry.key) { mutableStateOf<String?>(null) }
|
||||
var fetching by remember(entry.key) { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val git = entry.git
|
||||
|
||||
// Only for a project actually in a checkout; asking git about a
|
||||
@@ -2250,7 +2266,6 @@ private fun CheckoutSection(
|
||||
}
|
||||
}
|
||||
|
||||
SettingsHeading("Checkout")
|
||||
when {
|
||||
git == null -> {
|
||||
SettingsNote("This project is not in a git repository, so there is nothing to move.")
|
||||
@@ -2264,25 +2279,14 @@ private fun CheckoutSection(
|
||||
"or stash on the build machine first."
|
||||
)
|
||||
busy -> SettingsNote("Something is already running on this checkout.")
|
||||
else ->
|
||||
SettingsNote(
|
||||
"Moves the checkout on the build machine and builds what that leaves behind, the " +
|
||||
"same as Pull does. Picking a commit parks it there, on no branch; HEAD is " +
|
||||
"the way back to following the branch."
|
||||
)
|
||||
}
|
||||
|
||||
val enabled = !busy && !git.dirty && refs != null
|
||||
// A fetch replaces the very lists these two are showing, so they are
|
||||
// disabled while it runs rather than left offering the old answer.
|
||||
val enabled = !busy && !git.dirty && refs != null && !fetching
|
||||
val loaded = refs
|
||||
if (loaded == null && failure == null) {
|
||||
SettingsNote("Reading this checkout's branches...")
|
||||
}
|
||||
// The server's own words, so selectable like every other machine
|
||||
// output here -- the fix is on the other machine.
|
||||
failure?.let { OutputText(it, style = MaterialTheme.typography.bodySmall) }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Branch", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
SettingRow("Branch") {
|
||||
BranchPicker(
|
||||
branches = loaded?.branches.orEmpty(),
|
||||
// The branch line on the card is the same answer, and it is
|
||||
@@ -2293,8 +2297,7 @@ private fun CheckoutSection(
|
||||
onSelect = onCheckout,
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Commit", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
SettingRow("Commit") {
|
||||
CommitPicker(
|
||||
commits = loaded?.commits.orEmpty(),
|
||||
head = loaded?.head,
|
||||
@@ -2306,6 +2309,81 @@ private fun CheckoutSection(
|
||||
onSelect = onCheckout,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
fetching = true
|
||||
failure = null
|
||||
scope.launch {
|
||||
try {
|
||||
refs = withContext(Dispatchers.IO) { fetchRefs(entry.key) }
|
||||
} catch (e: DownloadServerException) {
|
||||
failure = e.message ?: "the fetch failed"
|
||||
} finally {
|
||||
fetching = false
|
||||
}
|
||||
}
|
||||
},
|
||||
// Not while the checkout is being worked on: a pull is
|
||||
// writing the same refs, and git would refuse one of the two
|
||||
// for a reason nobody pressed anything to hear. Uncommitted
|
||||
// work is deliberately *not* a reason -- a fetch touches no
|
||||
// file in the working tree, so this is the one control in
|
||||
// here that still does its job on a dirty checkout.
|
||||
enabled = !busy && !fetching,
|
||||
colors = ActionTone.Primary.colors(),
|
||||
) {
|
||||
if (fetching) {
|
||||
// The button's own content colour, which while it is
|
||||
// running is the disabled one -- a full-strength mark
|
||||
// beside a dimmed label reads as two things happening
|
||||
// rather than as one button that is busy.
|
||||
Working(color = LocalContentColor.current)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
Text("Fetch")
|
||||
}
|
||||
}
|
||||
// Both of these belong *below* the controls rather than above them.
|
||||
// Above, each one appears and disappears in the middle of the sheet
|
||||
// and shoves the two pickers down the screen as it does -- so the row
|
||||
// somebody is reaching for moves while they reach, and the failure
|
||||
// ends up nowhere near the button that produced it.
|
||||
if (loaded == null && failure == null) {
|
||||
SettingsNote("Reading this checkout's branches...")
|
||||
}
|
||||
// The server's own words, so selectable like every other machine
|
||||
// output here -- the fix is on the other machine.
|
||||
failure?.let { OutputText(it, style = MaterialTheme.typography.bodySmall) }
|
||||
}
|
||||
|
||||
/**
|
||||
* One setting: what it is on the left, the control for it on the right.
|
||||
*
|
||||
* The two get [Modifier.weight] rather than the control being left to take whatever width it likes,
|
||||
* because a picker's label is a branch name and those run long: unweighted, the pill grows to fit
|
||||
* `second-branch-from-elsewhere` and squeezes "Branch" down to a column three characters wide,
|
||||
* which then wraps one letter per line. Both sides truncate instead -- the control at its own end,
|
||||
* since a pill that has to be cut is still recognisably a control, and the name it is showing is
|
||||
* the thing the reader can open it to see in full.
|
||||
*/
|
||||
@Composable
|
||||
private fun SettingRow(label: String, control: @Composable () -> Unit) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(Modifier.weight(2f), contentAlignment = Alignment.CenterEnd) { control() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -2610,7 +2688,7 @@ private fun ComponentBuildButton(
|
||||
enabled = !state.busy && !projectState.busy,
|
||||
colors = ActionTone.Caution.colors(),
|
||||
) {
|
||||
Text("Rebuild")
|
||||
Text(buildWord(listOf(component)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user