|
@@ -36,6 +36,7 @@ namespace ClankerSaddle.Services {
|
|
|
public const string TASK_KIND_TASK = "task";
|
|
public const string TASK_KIND_TASK = "task";
|
|
|
public const string TASK_KIND_GOAL_RUN = "goal_run";
|
|
public const string TASK_KIND_GOAL_RUN = "goal_run";
|
|
|
|
|
|
|
|
|
|
+
|
|
|
/** Task creators: enqueueing is creator-only. */
|
|
/** Task creators: enqueueing is creator-only. */
|
|
|
public const string CREATOR_HUMAN = "human";
|
|
public const string CREATOR_HUMAN = "human";
|
|
|
public const string CREATOR_GOAL = "goal";
|
|
public const string CREATOR_GOAL = "goal";
|
|
@@ -1075,9 +1076,11 @@ namespace ClankerSaddle.Services {
|
|
|
* F9): the accept REFUSES while any daemon-reported goal
|
|
* F9): the accept REFUSES while any daemon-reported goal
|
|
|
* branch is unpushed — push-after-each-merge is enforced at
|
|
* branch is unpushed — push-after-each-merge is enforced at
|
|
|
* the operator's confirmation, read from the LIVE per-saddle
|
|
* the operator's confirmation, read from the LIVE per-saddle
|
|
|
- * probe flags (never a stored snapshot). Unknown flags (an
|
|
|
|
|
- * older daemon that never reported goal_branches) leave the
|
|
|
|
|
- * daemon-side push rule as the enforcing layer.
|
|
|
|
|
|
|
+ * probe flags (never a stored snapshot) through the ONE
|
|
|
|
|
+ * {@link parse_goal_branches} parser (the same rows the goal
|
|
|
|
|
+ * page's chips render). Unknown flags (an older daemon that
|
|
|
|
|
+ * never reported goal_branches) leave the daemon-side push
|
|
|
|
|
+ * rule as the enforcing layer.
|
|
|
*/
|
|
*/
|
|
|
private async void refuse_unpushed_origin(Goal goal, TaskEntity run) throws Error {
|
|
private async void refuse_unpushed_origin(Goal goal, TaskEntity run) throws Error {
|
|
|
int64 saddle_id = 0;
|
|
int64 saddle_id = 0;
|
|
@@ -1101,38 +1104,10 @@ namespace ClankerSaddle.Services {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
var unpushed = new Vector<string>();
|
|
var unpushed = new Vector<string>();
|
|
|
- try {
|
|
|
|
|
- var node = Json.from_string(raw);
|
|
|
|
|
- if (node.get_node_type() == Json.NodeType.ARRAY) {
|
|
|
|
|
- foreach (var element in node.get_array().get_elements()) {
|
|
|
|
|
- if (element.get_node_type() != Json.NodeType.OBJECT) {
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- var obj = element.get_object();
|
|
|
|
|
- string branch = obj.has_member("branch")
|
|
|
|
|
- ? (obj.get_string_member("branch") ?? "") : "";
|
|
|
|
|
- string repo = obj.has_member("repo")
|
|
|
|
|
- ? (obj.get_string_member("repo") ?? "") : "";
|
|
|
|
|
- string label = repo.length > 0 && branch.length > 0
|
|
|
|
|
- ? @"$(repo): $(branch)"
|
|
|
|
|
- : (branch.length > 0 ? branch : repo);
|
|
|
|
|
- bool pushed = obj.has_member("pushed")
|
|
|
|
|
- ? goal_node_truthy(obj.get_member("pushed")) : false;
|
|
|
|
|
- if (label.length > 0 && !pushed) {
|
|
|
|
|
- unpushed.add(label);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } else if (node.get_node_type() == Json.NodeType.OBJECT) {
|
|
|
|
|
- foreach (var member in node.get_object().get_members()) {
|
|
|
|
|
- var value = node.get_object().get_member(member);
|
|
|
|
|
- if (value.get_node_type() == Json.NodeType.VALUE
|
|
|
|
|
- && !goal_node_truthy(value)) {
|
|
|
|
|
- unpushed.add(member);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ foreach (var row in parse_goal_branches(raw)) {
|
|
|
|
|
+ if (!row.pushed) {
|
|
|
|
|
+ unpushed.add(row.label());
|
|
|
}
|
|
}
|
|
|
- } catch (Error parse_error) {
|
|
|
|
|
- return;
|
|
|
|
|
}
|
|
}
|
|
|
if (unpushed.length > 0) {
|
|
if (unpushed.length > 0) {
|
|
|
var labels = new StringBuilder();
|
|
var labels = new StringBuilder();
|
|
@@ -1147,27 +1122,81 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** A JSON value as a boolean truth (objects/arrays: present). */
|
|
|
|
|
- private static bool goal_node_truthy(Json.Node node) {
|
|
|
|
|
- if (node.get_node_type() == Json.NodeType.VALUE) {
|
|
|
|
|
- var value = node.get_value();
|
|
|
|
|
- if (value.type() == typeof(bool)) {
|
|
|
|
|
- return ((!)value).get_boolean();
|
|
|
|
|
- }
|
|
|
|
|
- if (value.type() == typeof(string)) {
|
|
|
|
|
- return ((!)value).dup_string() != "false";
|
|
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // Helpers
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * One daemon-reported goal-branch push flag: a row of the
|
|
|
|
|
+ * /status `goal_branches` array (the ONLY shape the daemon
|
|
|
|
|
+ * emits — one `{repo, branch, pushed}` row per involved
|
|
|
|
|
+ * repo).
|
|
|
|
|
+ */
|
|
|
|
|
+ public class GoalBranchPush : Object {
|
|
|
|
|
+ public string repo { get; set; default = ""; }
|
|
|
|
|
+ public string branch { get; set; default = ""; }
|
|
|
|
|
+ public bool pushed { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ /** The display/gate label: "repo: branch" when both are known. */
|
|
|
|
|
+ public string label() {
|
|
|
|
|
+ return repo.length > 0 && branch.length > 0
|
|
|
|
|
+ ? @"$(repo): $(branch)" : (branch.length > 0 ? branch : repo);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The ONE parser of the daemon's goal_branches report (plan
|
|
|
|
|
+ * 15 C4): an array of `{repo, branch, pushed}` rows, read by
|
|
|
|
|
+ * BOTH consumers — the completion-accept gate
|
|
|
|
|
+ * ({@link refuse_unpushed_origin}) and the goal page's push
|
|
|
|
|
+ * chips — so the two can never diverge on the same payload
|
|
|
|
|
+ * again. `pushed` is the daemon's boolean (an integer counts
|
|
|
|
|
+ * as non-zero; anything else reads unpushed). A row missing
|
|
|
|
|
+ * its branch labels by its repo alone; label-less rows drop.
|
|
|
|
|
+ * Unparseable input parses as no rows.
|
|
|
|
|
+ */
|
|
|
|
|
+ public static Vector<GoalBranchPush> parse_goal_branches(string raw) {
|
|
|
|
|
+ var rows = new Vector<GoalBranchPush>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ var node = Json.from_string(raw);
|
|
|
|
|
+ if (node.get_node_type() != Json.NodeType.ARRAY) {
|
|
|
|
|
+ return rows;
|
|
|
}
|
|
}
|
|
|
- if (value.type() == typeof(int64)) {
|
|
|
|
|
- return ((!)value).get_int64() != 0;
|
|
|
|
|
|
|
+ foreach (var element in node.get_array().get_elements()) {
|
|
|
|
|
+ if (element.get_node_type() != Json.NodeType.OBJECT) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ var obj = element.get_object();
|
|
|
|
|
+ var row = new GoalBranchPush();
|
|
|
|
|
+ row.repo = obj.has_member("repo")
|
|
|
|
|
+ ? (obj.get_string_member("repo") ?? "") : "";
|
|
|
|
|
+ if (obj.has_member("branch")) {
|
|
|
|
|
+ row.branch = obj.get_string_member("branch") ?? "";
|
|
|
|
|
+ } else if (row.repo.length > 0) {
|
|
|
|
|
+ row.branch = row.repo;
|
|
|
|
|
+ row.repo = "";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (obj.has_member("pushed")) {
|
|
|
|
|
+ var pushed = obj.get_member("pushed");
|
|
|
|
|
+ if (pushed.get_node_type() == Json.NodeType.VALUE) {
|
|
|
|
|
+ var value = pushed.get_value();
|
|
|
|
|
+ if (value.type() == typeof(bool)) {
|
|
|
|
|
+ row.pushed = ((!)value).get_boolean();
|
|
|
|
|
+ } else if (value.type() == typeof(int64)) {
|
|
|
|
|
+ row.pushed = ((!)value).get_int64() != 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (row.label().length > 0) {
|
|
|
|
|
+ rows.add(row);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ // Unparseable push state reads as no rows.
|
|
|
}
|
|
}
|
|
|
- return true;
|
|
|
|
|
|
|
+ return rows;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ------------------------------------------------------------------
|
|
|
|
|
- // Helpers
|
|
|
|
|
- // ------------------------------------------------------------------
|
|
|
|
|
-
|
|
|
|
|
private async Goal require_goal(int64 goal_id) throws Error {
|
|
private async Goal require_goal(int64 goal_id) throws Error {
|
|
|
var goal = yield get_goal(goal_id);
|
|
var goal = yield get_goal(goal_id);
|
|
|
if (goal == null) {
|
|
if (goal == null) {
|