huskies: merge 581_story_freeze_command_to_hold_a_story_at_its_current_stage_without_advancing
This commit is contained in:
@@ -15,7 +15,7 @@ use super::scan::{
|
|||||||
};
|
};
|
||||||
use super::story_checks::{
|
use super::story_checks::{
|
||||||
check_archived_dependencies, has_merge_failure, has_review_hold, has_unmet_dependencies,
|
check_archived_dependencies, has_merge_failure, has_review_hold, has_unmet_dependencies,
|
||||||
is_story_blocked, read_story_front_matter_agent,
|
is_story_blocked, is_story_frozen, read_story_front_matter_agent,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl AgentPool {
|
impl AgentPool {
|
||||||
@@ -103,6 +103,12 @@ impl AgentPool {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip frozen stories — pipeline advancement is suspended.
|
||||||
|
if is_story_frozen(project_root, stage_dir, story_id) {
|
||||||
|
slog!("[auto-assign] Story '{story_id}' is frozen; skipping until unfrozen.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip blocked stories (retry limit exceeded).
|
// Skip blocked stories (retry limit exceeded).
|
||||||
if is_story_blocked(project_root, stage_dir, story_id) {
|
if is_story_blocked(project_root, stage_dir, story_id) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -93,6 +93,19 @@ pub(super) fn check_archived_dependencies(
|
|||||||
crate::io::story_metadata::check_archived_deps(project_root, stage_dir, story_id)
|
crate::io::story_metadata::check_archived_deps(project_root, stage_dir, story_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the story file has `frozen: true` in its front matter.
|
||||||
|
pub(super) fn is_story_frozen(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
|
||||||
|
use crate::io::story_metadata::parse_front_matter;
|
||||||
|
let contents = match read_story_contents(project_root, story_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
parse_front_matter(&contents)
|
||||||
|
.ok()
|
||||||
|
.and_then(|m| m.frozen)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if the story file has a `merge_failure` field in its front matter.
|
/// Return `true` if the story file has a `merge_failure` field in its front matter.
|
||||||
pub(super) fn has_merge_failure(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
|
pub(super) fn has_merge_failure(project_root: &Path, _stage_dir: &str, story_id: &str) -> bool {
|
||||||
use crate::io::story_metadata::parse_front_matter;
|
use crate::io::story_metadata::parse_front_matter;
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ impl AgentPool {
|
|||||||
.map(agent_config_stage)
|
.map(agent_config_stage)
|
||||||
.unwrap_or_else(|| pipeline_stage(agent_name));
|
.unwrap_or_else(|| pipeline_stage(agent_name));
|
||||||
|
|
||||||
|
// If the story is frozen, do not advance the pipeline. The agent's work
|
||||||
|
// is done but the story stays at its current stage.
|
||||||
|
if crate::io::story_metadata::is_story_frozen_in_store(story_id) {
|
||||||
|
slog!("[pipeline] Story '{story_id}' is frozen; pipeline advancement suppressed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match stage {
|
match stage {
|
||||||
PipelineStage::Other => {
|
PipelineStage::Other => {
|
||||||
// Supervisors and unknown agents do not advance the pipeline.
|
// Supervisors and unknown agents do not advance the pipeline.
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
//! Handler for the `freeze` and `unfreeze` commands.
|
||||||
|
//!
|
||||||
|
//! `freeze <number>` sets `frozen: true` on the story, halting pipeline
|
||||||
|
//! advancement and auto-assign until `unfreeze <number>` clears the flag.
|
||||||
|
|
||||||
|
use super::CommandContext;
|
||||||
|
use crate::io::story_metadata::{
|
||||||
|
clear_front_matter_field_in_content, parse_front_matter, set_front_matter_field,
|
||||||
|
};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Handle the `freeze` command.
|
||||||
|
///
|
||||||
|
/// Parses `<number>` from `ctx.args`, locates the work item, and sets
|
||||||
|
/// `frozen: true` in its front matter.
|
||||||
|
pub(super) fn handle_freeze(ctx: &CommandContext) -> Option<String> {
|
||||||
|
let num_str = ctx.args.trim();
|
||||||
|
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
return Some(format!(
|
||||||
|
"Usage: `{} freeze <number>` (e.g. `freeze 42`)",
|
||||||
|
ctx.bot_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(freeze_by_number(ctx.project_root, num_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core freeze logic: find story by numeric prefix and set `frozen: true`.
|
||||||
|
///
|
||||||
|
/// Returns a Markdown-formatted response string suitable for all transports.
|
||||||
|
pub(crate) fn freeze_by_number(project_root: &Path, story_number: &str) -> String {
|
||||||
|
let (story_id, _, _, _) =
|
||||||
|
match crate::chat::lookup::find_story_by_number(project_root, story_number) {
|
||||||
|
Some(found) => found,
|
||||||
|
None => {
|
||||||
|
return format!("No story, bug, or spike with number **{story_number}** found.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
freeze_by_story_id(&story_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn freeze_by_story_id(story_id: &str) -> String {
|
||||||
|
let contents = match crate::db::read_content(story_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return format!("Failed to read story content for **{story_id}**"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = match parse_front_matter(&contents) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return format!("Failed to parse front matter for **{story_id}**: {e}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let story_name = meta.name.as_deref().unwrap_or(story_id).to_string();
|
||||||
|
|
||||||
|
if meta.frozen == Some(true) {
|
||||||
|
return format!("**{story_name}** ({story_id}) is already frozen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = set_front_matter_field(&contents, "frozen", "true");
|
||||||
|
|
||||||
|
crate::db::write_content(story_id, &updated);
|
||||||
|
let stage = crate::pipeline_state::read_typed(story_id)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|i| i.stage.dir_name().to_string())
|
||||||
|
.unwrap_or_else(|| "2_current".to_string());
|
||||||
|
crate::db::write_item_with_content(story_id, &stage, &updated);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Frozen **{story_name}** ({story_id}). Pipeline advancement and auto-assign suppressed until unfrozen."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the `unfreeze` command.
|
||||||
|
///
|
||||||
|
/// Parses `<number>` from `ctx.args`, locates the work item, and clears the
|
||||||
|
/// `frozen` flag to resume normal pipeline behaviour.
|
||||||
|
pub(super) fn handle_unfreeze(ctx: &CommandContext) -> Option<String> {
|
||||||
|
let num_str = ctx.args.trim();
|
||||||
|
if num_str.is_empty() || !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
return Some(format!(
|
||||||
|
"Usage: `{} unfreeze <number>` (e.g. `unfreeze 42`)",
|
||||||
|
ctx.bot_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(unfreeze_by_number(ctx.project_root, num_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core unfreeze logic: find story by numeric prefix and clear `frozen` flag.
|
||||||
|
pub(crate) fn unfreeze_by_number(project_root: &Path, story_number: &str) -> String {
|
||||||
|
let (story_id, _, _, _) =
|
||||||
|
match crate::chat::lookup::find_story_by_number(project_root, story_number) {
|
||||||
|
Some(found) => found,
|
||||||
|
None => {
|
||||||
|
return format!("No story, bug, or spike with number **{story_number}** found.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unfreeze_by_story_id(&story_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unfreeze_by_story_id(story_id: &str) -> String {
|
||||||
|
let contents = match crate::db::read_content(story_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return format!("Failed to read story content for **{story_id}**"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = match parse_front_matter(&contents) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return format!("Failed to parse front matter for **{story_id}**: {e}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let story_name = meta.name.as_deref().unwrap_or(story_id).to_string();
|
||||||
|
|
||||||
|
if meta.frozen != Some(true) {
|
||||||
|
return format!("**{story_name}** ({story_id}) is not frozen. Nothing to unfreeze.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = clear_front_matter_field_in_content(&contents, "frozen");
|
||||||
|
|
||||||
|
crate::db::write_content(story_id, &updated);
|
||||||
|
let stage = crate::pipeline_state::read_typed(story_id)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|i| i.stage.dir_name().to_string())
|
||||||
|
.unwrap_or_else(|| "2_current".to_string());
|
||||||
|
crate::db::write_item_with_content(story_id, &stage, &updated);
|
||||||
|
|
||||||
|
format!("Unfrozen **{story_name}** ({story_id}). Normal pipeline behaviour resumed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::agents::AgentPool;
|
||||||
|
use crate::chat::test_helpers::write_story_file;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use super::super::{CommandDispatch, try_handle_command};
|
||||||
|
|
||||||
|
fn freeze_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
|
let agents = Arc::new(AgentPool::new_test(3000));
|
||||||
|
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||||
|
let room_id = "!test:example.com".to_string();
|
||||||
|
let dispatch = CommandDispatch {
|
||||||
|
bot_name: "Timmy",
|
||||||
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
|
project_root: root,
|
||||||
|
agents: &agents,
|
||||||
|
ambient_rooms: &ambient_rooms,
|
||||||
|
room_id: &room_id,
|
||||||
|
};
|
||||||
|
try_handle_command(&dispatch, &format!("@timmy freeze {args}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unfreeze_cmd_with_root(root: &std::path::Path, args: &str) -> Option<String> {
|
||||||
|
let agents = Arc::new(AgentPool::new_test(3000));
|
||||||
|
let ambient_rooms = Arc::new(Mutex::new(HashSet::new()));
|
||||||
|
let room_id = "!test:example.com".to_string();
|
||||||
|
let dispatch = CommandDispatch {
|
||||||
|
bot_name: "Timmy",
|
||||||
|
bot_user_id: "@timmy:homeserver.local",
|
||||||
|
project_root: root,
|
||||||
|
agents: &agents,
|
||||||
|
ambient_rooms: &ambient_rooms,
|
||||||
|
room_id: &room_id,
|
||||||
|
};
|
||||||
|
try_handle_command(&dispatch, &format!("@timmy unfreeze {args}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_command_is_registered() {
|
||||||
|
use super::super::commands;
|
||||||
|
assert!(
|
||||||
|
commands().iter().any(|c| c.name == "freeze"),
|
||||||
|
"freeze command must be in the registry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unfreeze_command_is_registered() {
|
||||||
|
use super::super::commands;
|
||||||
|
assert!(
|
||||||
|
commands().iter().any(|c| c.name == "unfreeze"),
|
||||||
|
"unfreeze command must be in the registry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_command_no_args_returns_usage() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
let output = freeze_cmd_with_root(tmp.path(), "").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("Usage"),
|
||||||
|
"no args should show usage: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unfreeze_command_no_args_returns_usage() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
let output = unfreeze_cmd_with_root(tmp.path(), "").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("Usage"),
|
||||||
|
"no args should show usage: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_command_not_found_returns_error() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
let output = freeze_cmd_with_root(tmp.path(), "9988").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("9988") && output.contains("found"),
|
||||||
|
"not-found message should include number and 'found': {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_command_sets_frozen_flag() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
write_story_file(
|
||||||
|
tmp.path(),
|
||||||
|
"2_current",
|
||||||
|
"9940_story_freezeme.md",
|
||||||
|
"---\nname: Freeze Me\n---\n# Story\n",
|
||||||
|
);
|
||||||
|
let output = freeze_cmd_with_root(tmp.path(), "9940").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("Frozen") && output.contains("Freeze Me"),
|
||||||
|
"should confirm freeze with story name: {output}"
|
||||||
|
);
|
||||||
|
let contents = crate::db::read_content("9940_story_freezeme")
|
||||||
|
.expect("story content should be readable after freeze");
|
||||||
|
assert!(
|
||||||
|
contents.contains("frozen: true"),
|
||||||
|
"frozen flag should be set: {contents}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unfreeze_command_clears_frozen_flag() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
crate::db::ensure_content_store();
|
||||||
|
write_story_file(
|
||||||
|
tmp.path(),
|
||||||
|
"2_current",
|
||||||
|
"9941_story_frozen.md",
|
||||||
|
"---\nname: Frozen Story\nfrozen: true\n---\n# Story\n",
|
||||||
|
);
|
||||||
|
let output = unfreeze_cmd_with_root(tmp.path(), "9941").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("Unfrozen") && output.contains("Frozen Story"),
|
||||||
|
"should confirm unfreeze with story name: {output}"
|
||||||
|
);
|
||||||
|
let contents = crate::db::read_content("9941_story_frozen")
|
||||||
|
.expect("story content should be readable after unfreeze");
|
||||||
|
assert!(
|
||||||
|
!contents.contains("frozen:"),
|
||||||
|
"frozen flag should be removed: {contents}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unfreeze_command_not_frozen_returns_error() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
write_story_file(
|
||||||
|
tmp.path(),
|
||||||
|
"2_current",
|
||||||
|
"9942_story_notfrozen.md",
|
||||||
|
"---\nname: Not Frozen\n---\n# Story\n",
|
||||||
|
);
|
||||||
|
let output = unfreeze_cmd_with_root(tmp.path(), "9942").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("not frozen"),
|
||||||
|
"should return not-frozen error: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freeze_command_already_frozen_returns_message() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
write_story_file(
|
||||||
|
tmp.path(),
|
||||||
|
"2_current",
|
||||||
|
"9943_story_alreadyfrozen.md",
|
||||||
|
"---\nname: Already Frozen\nfrozen: true\n---\n# Story\n",
|
||||||
|
);
|
||||||
|
let output = freeze_cmd_with_root(tmp.path(), "9943").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("already frozen"),
|
||||||
|
"should say already frozen: {output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ mod backlog;
|
|||||||
mod cost;
|
mod cost;
|
||||||
mod coverage;
|
mod coverage;
|
||||||
mod depends;
|
mod depends;
|
||||||
|
mod freeze;
|
||||||
mod git;
|
mod git;
|
||||||
mod help;
|
mod help;
|
||||||
pub(crate) mod loc;
|
pub(crate) mod loc;
|
||||||
@@ -203,6 +204,16 @@ pub fn commands() -> &'static [BotCommand] {
|
|||||||
description: "Reset a blocked story: `unblock <number>` (clears blocked flag and resets retry count)",
|
description: "Reset a blocked story: `unblock <number>` (clears blocked flag and resets retry count)",
|
||||||
handler: unblock::handle_unblock,
|
handler: unblock::handle_unblock,
|
||||||
},
|
},
|
||||||
|
BotCommand {
|
||||||
|
name: "freeze",
|
||||||
|
description: "Freeze a story at its current stage: `freeze <number>` (suppresses pipeline advancement and auto-assign)",
|
||||||
|
handler: freeze::handle_freeze,
|
||||||
|
},
|
||||||
|
BotCommand {
|
||||||
|
name: "unfreeze",
|
||||||
|
description: "Unfreeze a story: `unfreeze <number>` (resumes normal pipeline behaviour)",
|
||||||
|
handler: freeze::handle_unfreeze,
|
||||||
|
},
|
||||||
BotCommand {
|
BotCommand {
|
||||||
name: "unreleased",
|
name: "unreleased",
|
||||||
description: "Show stories merged to master since the last release tag",
|
description: "Show stories merged to master since the last release tag",
|
||||||
|
|||||||
@@ -228,7 +228,13 @@ fn render_item_line(
|
|||||||
} else {
|
} else {
|
||||||
Some(item.name.as_str())
|
Some(item.name.as_str())
|
||||||
};
|
};
|
||||||
let display = story_short_label(story_id, name_opt);
|
let frozen = crate::io::story_metadata::is_story_frozen_in_store(story_id);
|
||||||
|
let base_label = story_short_label(story_id, name_opt);
|
||||||
|
let display = if frozen {
|
||||||
|
format!("\u{2744}\u{FE0F} {base_label}") // ❄️ prefix
|
||||||
|
} else {
|
||||||
|
base_label
|
||||||
|
};
|
||||||
let cost_suffix = cost_by_story
|
let cost_suffix = cost_by_story
|
||||||
.get(story_id)
|
.get(story_id)
|
||||||
.filter(|&&c| c > 0.0)
|
.filter(|&&c| c > 0.0)
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ pub struct StoryMetadata {
|
|||||||
/// Story numbers this story depends on. Auto-assign will skip this story
|
/// Story numbers this story depends on. Auto-assign will skip this story
|
||||||
/// until all dependencies have reached `5_done` or `6_archived`.
|
/// until all dependencies have reached `5_done` or `6_archived`.
|
||||||
pub depends_on: Option<Vec<u32>>,
|
pub depends_on: Option<Vec<u32>>,
|
||||||
|
/// When `true`, the story is frozen: auto-assign skips it, the pipeline
|
||||||
|
/// does not advance it, and no mergemaster is spawned.
|
||||||
|
pub frozen: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -89,6 +92,8 @@ struct FrontMatter {
|
|||||||
blocked: Option<bool>,
|
blocked: Option<bool>,
|
||||||
/// Story numbers this story depends on.
|
/// Story numbers this story depends on.
|
||||||
depends_on: Option<Vec<u32>>,
|
depends_on: Option<Vec<u32>>,
|
||||||
|
/// When `true`, the story is frozen.
|
||||||
|
frozen: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
||||||
@@ -129,6 +134,7 @@ fn build_metadata(front: FrontMatter) -> StoryMetadata {
|
|||||||
retry_count: front.retry_count,
|
retry_count: front.retry_count,
|
||||||
blocked: front.blocked,
|
blocked: front.blocked,
|
||||||
depends_on: front.depends_on,
|
depends_on: front.depends_on,
|
||||||
|
frozen: front.frozen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +445,20 @@ pub fn increment_retry_count_in_content(contents: &str) -> (String, u32) {
|
|||||||
(updated, new_count)
|
(updated, new_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` if the story has `frozen: true` in the content store.
|
||||||
|
///
|
||||||
|
/// Used by the pipeline advance code to suppress stage transitions for frozen stories.
|
||||||
|
pub fn is_story_frozen_in_store(story_id: &str) -> bool {
|
||||||
|
let contents = match crate::db::read_content(story_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
parse_front_matter(&contents)
|
||||||
|
.ok()
|
||||||
|
.and_then(|m| m.frozen)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Write `blocked: true` to story content (pure function).
|
/// Write `blocked: true` to story content (pure function).
|
||||||
pub fn write_blocked_in_content(contents: &str) -> String {
|
pub fn write_blocked_in_content(contents: &str) -> String {
|
||||||
set_front_matter_field(contents, "blocked", "true")
|
set_front_matter_field(contents, "blocked", "true")
|
||||||
|
|||||||
Reference in New Issue
Block a user