huskies: merge 1069
This commit is contained in:
+30
-6
@@ -124,19 +124,43 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Categorise merged work items and format names.
|
# Categorise merged work items and format names.
|
||||||
|
# Supports two subject formats (after stripping the "huskies: merge " prefix):
|
||||||
|
# New: "1063 story Human Readable Name"
|
||||||
|
# Old: "1063_story_human_readable_name"
|
||||||
FEATURES=""
|
FEATURES=""
|
||||||
FIXES=""
|
FIXES=""
|
||||||
REFACTORS=""
|
REFACTORS=""
|
||||||
while IFS= read -r item; do
|
while IFS= read -r item; do
|
||||||
[ -z "$item" ] && continue
|
[ -z "$item" ] && continue
|
||||||
# Strip the numeric prefix and type to get the human name.
|
|
||||||
name=$(echo "$item" | sed -E 's/^[0-9]+_(story|bug|refactor|spike)_//' | tr '_' ' ')
|
# Extract the leading numeric ID (present in both formats).
|
||||||
|
id=$(echo "$item" | grep -oE '^[0-9]+')
|
||||||
|
|
||||||
|
# Detect format and extract human name + type word.
|
||||||
|
if echo "$item" | grep -qE '^[0-9]+ (story|bug|refactor|spike|epic) '; then
|
||||||
|
# New format: "1063 story Human Name Here"
|
||||||
|
type_word=$(echo "$item" | sed -E 's/^[0-9]+ ([a-z]+) .*/\1/')
|
||||||
|
name=$(echo "$item" | sed -E 's/^[0-9]+ [a-z]+ //')
|
||||||
|
else
|
||||||
|
# Legacy slug format: "1063_story_human_name_here"
|
||||||
|
type_word=$(echo "$item" | sed -E 's/^[0-9]+_([a-z]+)_.*/\1/')
|
||||||
|
name=$(echo "$item" | sed -E 's/^[0-9]+_(story|bug|refactor|spike|epic)_//' | tr '_' ' ')
|
||||||
|
fi
|
||||||
|
|
||||||
# Capitalise first letter.
|
# Capitalise first letter.
|
||||||
name="$(echo "${name:0:1}" | tr '[:lower:]' '[:upper:]')${name:1}"
|
name="$(echo "${name:0:1}" | tr '[:lower:]' '[:upper:]')${name:1}"
|
||||||
case "$item" in
|
|
||||||
*_bug_*) FIXES="${FIXES}- ${name}\n" ;;
|
# Format as "Name (ID)" when a numeric ID was found, plain name otherwise.
|
||||||
*_refactor_*) REFACTORS="${REFACTORS}- ${name}\n" ;;
|
if [ -n "$id" ]; then
|
||||||
*) FEATURES="${FEATURES}- ${name}\n" ;;
|
entry="${name} (${id})"
|
||||||
|
else
|
||||||
|
entry="${name}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$type_word" in
|
||||||
|
bug) FIXES="${FIXES}- ${entry}\n" ;;
|
||||||
|
refactor) REFACTORS="${REFACTORS}- ${entry}\n" ;;
|
||||||
|
*) FEATURES="${FEATURES}- ${entry}\n" ;;
|
||||||
esac
|
esac
|
||||||
done <<< "$MERGED_RAW"
|
done <<< "$MERGED_RAW"
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,15 @@ pub(crate) fn run_squash_merge(
|
|||||||
|
|
||||||
// ── Commit in the temporary worktree ──────────────────────────
|
// ── Commit in the temporary worktree ──────────────────────────
|
||||||
all_output.push_str("=== git commit ===\n");
|
all_output.push_str("=== git commit ===\n");
|
||||||
let commit_msg = format!("huskies: merge {story_id}");
|
// Include human-readable name and item type when the CRDT is available.
|
||||||
|
// Falls back to the bare ID when running outside the server (e.g. in tests).
|
||||||
|
let story_label = crate::crdt_state::read_item(story_id)
|
||||||
|
.map(|item| {
|
||||||
|
let type_str = item.item_type().map(|t| t.as_str()).unwrap_or("story");
|
||||||
|
format!(" {} {}", type_str, item.name())
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let commit_msg = format!("huskies: merge {story_id}{story_label}");
|
||||||
let commit = Command::new("git")
|
let commit = Command::new("git")
|
||||||
.args(["commit", "-m", &commit_msg])
|
.args(["commit", "-m", &commit_msg])
|
||||||
.current_dir(&merge_wt_path)
|
.current_dir(&merge_wt_path)
|
||||||
@@ -507,3 +515,5 @@ fn run_merge_quality_gates(
|
|||||||
mod tests_advanced;
|
mod tests_advanced;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_basic;
|
mod tests_basic;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests_changelog;
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
//! Regression tests for changelog entry parsing — both legacy-slug and new-format
|
||||||
|
//! merge commit subjects must resolve to a human-readable "Name (ID)" entry.
|
||||||
|
|
||||||
|
/// Parse a single merge commit subject (after stripping the `huskies: merge ` prefix)
|
||||||
|
/// into `(id, type_word, human_name)`.
|
||||||
|
///
|
||||||
|
/// Returns `None` for subjects that are not recognised merge items.
|
||||||
|
fn parse_changelog_entry(item: &str) -> Option<(String, String, String)> {
|
||||||
|
let item = item.trim();
|
||||||
|
if item.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract leading numeric ID present in both formats.
|
||||||
|
let id: String = item.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||||
|
if id.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect format by the character immediately following the digits.
|
||||||
|
// id contains only ASCII digits so id.len() is a valid char boundary.
|
||||||
|
let rest = item.get(id.len()..).unwrap_or("");
|
||||||
|
if let Some(space_rest) = rest.strip_prefix(' ') {
|
||||||
|
// New format: "1063 story Human Name Here"
|
||||||
|
let mut words = space_rest.splitn(2, ' ');
|
||||||
|
let type_word = words.next().unwrap_or("story").to_string();
|
||||||
|
let name = words.next().unwrap_or("").trim().to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((id, type_word, name))
|
||||||
|
} else if let Some(slug_rest) = rest.strip_prefix('_') {
|
||||||
|
// Legacy slug format: "1063_story_human_name_here"
|
||||||
|
let mut parts = slug_rest.splitn(2, '_');
|
||||||
|
let type_word = parts.next().unwrap_or("story").to_string();
|
||||||
|
let slug = parts.next().unwrap_or("").replace('_', " ");
|
||||||
|
if slug.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((id, type_word, slug))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a parsed entry as "Human Name (ID)".
|
||||||
|
fn format_entry(id: &str, name: &str) -> String {
|
||||||
|
let mut chars = name.chars();
|
||||||
|
let capitalised = match chars.next() {
|
||||||
|
None => String::new(),
|
||||||
|
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
|
||||||
|
};
|
||||||
|
format!("{capitalised} ({id})")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_new_format_story_resolves_to_name_and_id() {
|
||||||
|
let item = "1063 story Tee pipeline events into gateway context";
|
||||||
|
let (id, _type_word, name) = parse_changelog_entry(item).expect("should parse new format");
|
||||||
|
assert_eq!(id, "1063");
|
||||||
|
assert_eq!(
|
||||||
|
format_entry(&id, &name),
|
||||||
|
"Tee pipeline events into gateway context (1063)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_new_format_bug_resolves_to_name_and_id() {
|
||||||
|
let item = "999 bug Fix the broken auth token";
|
||||||
|
let (id, type_word, name) = parse_changelog_entry(item).expect("should parse new-format bug");
|
||||||
|
assert_eq!(id, "999");
|
||||||
|
assert_eq!(type_word, "bug");
|
||||||
|
assert_eq!(format_entry(&id, &name), "Fix the broken auth token (999)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_new_format_refactor_resolves_to_name_and_id() {
|
||||||
|
let item = "777 refactor Extract config parsing";
|
||||||
|
let (id, type_word, name) = parse_changelog_entry(item).expect("should parse refactor");
|
||||||
|
assert_eq!(type_word, "refactor");
|
||||||
|
assert_eq!(format_entry(&id, &name), "Extract config parsing (777)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_legacy_slug_story_resolves_to_name_and_id() {
|
||||||
|
let item = "1063_story_tee_pipeline_events_into_gateway_context";
|
||||||
|
let (id, _type_word, name) = parse_changelog_entry(item).expect("should parse legacy slug");
|
||||||
|
assert_eq!(id, "1063");
|
||||||
|
assert_eq!(
|
||||||
|
format_entry(&id, &name),
|
||||||
|
"Tee pipeline events into gateway context (1063)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_legacy_slug_bug_resolves_to_name_and_id() {
|
||||||
|
let item = "999_bug_fix_the_broken_auth_token";
|
||||||
|
let (id, type_word, name) = parse_changelog_entry(item).expect("should parse legacy bug slug");
|
||||||
|
assert_eq!(id, "999");
|
||||||
|
assert_eq!(type_word, "bug");
|
||||||
|
assert_eq!(format_entry(&id, &name), "Fix the broken auth token (999)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changelog_mixed_fixture_all_entries_have_human_names() {
|
||||||
|
// Fixture: a mix of legacy-slug and new-format subjects (as they appear
|
||||||
|
// after stripping the "huskies: merge " prefix from the git log).
|
||||||
|
let fixture = [
|
||||||
|
// Legacy slug formats (pre-migration)
|
||||||
|
"1001_story_add_matrix_transport",
|
||||||
|
"1002_bug_fix_crdt_sync_disconnect",
|
||||||
|
"1003_refactor_extract_gateway_config",
|
||||||
|
// New format (post-story-1069)
|
||||||
|
"1050 story Add agent pool auto-assign",
|
||||||
|
"1063 story Tee pipeline events into gateway context",
|
||||||
|
"1064 bug Stop lagged handler re-emitting via same channel",
|
||||||
|
"1065 refactor Move squash merge into own module",
|
||||||
|
];
|
||||||
|
|
||||||
|
for item in &fixture {
|
||||||
|
let result = parse_changelog_entry(item);
|
||||||
|
assert!(result.is_some(), "failed to parse merge subject: {item:?}");
|
||||||
|
let (id, _type_word, name) = result.unwrap();
|
||||||
|
let entry = format_entry(&id, &name);
|
||||||
|
// Every entry must contain the numeric ID in parentheses.
|
||||||
|
assert!(
|
||||||
|
entry.contains(&format!("({id})")),
|
||||||
|
"entry missing numeric ID: {entry:?}"
|
||||||
|
);
|
||||||
|
// Name must not be empty or just whitespace.
|
||||||
|
assert!(
|
||||||
|
!name.trim().is_empty(),
|
||||||
|
"empty human name for item: {item:?}"
|
||||||
|
);
|
||||||
|
// Name must not be a raw slug (contains underscores as word separators).
|
||||||
|
// (Underscores are OK inside words like "auto-assign" but not as spaces.)
|
||||||
|
assert!(
|
||||||
|
!name.contains('_'),
|
||||||
|
"name still contains underscores (slug not decoded): {name:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user