pub const GENERATED_START: &str = "// @ggen:generated-start";
pub const GENERATED_END: &str = "// @ggen:generated-end";
pub const PRESERVE_START: &str = "// @ggen:preserve-start";
pub const PRESERVE_END: &str = "// @ggen:preserve-end";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegionType {
Generated,
Custom,
Conflict,
}
#[derive(Debug, Clone)]
pub struct MergeRegion {
pub start_line: usize,
pub end_line: usize,
pub region_type: RegionType,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct MergeResult {
pub merged_content: String,
pub conflicts: Vec<MergeConflict>,
pub preserved_regions: usize,
pub regenerated_regions: usize,
}
#[derive(Debug, Clone)]
pub struct MergeConflict {
pub line: usize,
pub region: String,
pub base_content: String,
pub ours: String,
pub theirs: String,
}
#[derive(Debug)]
pub enum MergeError {
ParseError(String),
ConflictUnresolvable(Vec<MergeConflict>),
IoError(std::io::Error),
}
impl std::fmt::Display for MergeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MergeError::ParseError(msg) => write!(f, "parse error: {}", msg),
MergeError::ConflictUnresolvable(conflicts) => {
write!(f, "{} unresolvable conflict(s)", conflicts.len())
}
MergeError::IoError(e) => write!(f, "I/O error: {}", e),
}
}
}
impl std::error::Error for MergeError {}
pub fn parse_regions(content: &str) -> Result<Vec<MergeRegion>, MergeError> {
let lines: Vec<&str> = content.lines().collect();
let mut regions: Vec<MergeRegion> = Vec::new();
enum State {
Outside,
InGenerated { start: usize, buf: String },
InPreserve { start: usize, buf: String },
}
let mut unannotated_buf = String::new();
let mut unannotated_start = 1usize;
let mut state = State::Outside;
let flush_unannotated =
|buf: &mut String, start: usize, line_idx: usize, regions: &mut Vec<MergeRegion>| {
if !buf.is_empty() {
regions.push(MergeRegion {
start_line: start,
end_line: line_idx.saturating_sub(1),
region_type: RegionType::Generated, content: std::mem::take(buf),
});
}
};
for (idx, &line) in lines.iter().enumerate() {
let line_no = idx + 1;
match &mut state {
State::Outside => {
if line.trim_start().starts_with(GENERATED_START) {
flush_unannotated(
&mut unannotated_buf,
unannotated_start,
line_no,
&mut regions,
);
let mut buf = String::from(line);
buf.push('\n');
state = State::InGenerated {
start: line_no,
buf,
};
} else if line.trim_start().starts_with(PRESERVE_START) {
flush_unannotated(
&mut unannotated_buf,
unannotated_start,
line_no,
&mut regions,
);
let mut buf = String::from(line);
buf.push('\n');
state = State::InPreserve {
start: line_no,
buf,
};
} else {
if unannotated_buf.is_empty() {
unannotated_start = line_no;
}
unannotated_buf.push_str(line);
unannotated_buf.push('\n');
}
}
State::InGenerated { start, buf } => {
if line.trim_start().starts_with(PRESERVE_START) {
return Err(MergeError::ParseError(format!(
"line {}: nested @ggen:preserve-start inside a @ggen:generated block \
(started at line {}); nested preserve regions are not supported",
line_no, start
)));
}
buf.push_str(line);
buf.push('\n');
if line.trim_start().starts_with(GENERATED_END) {
let region = MergeRegion {
start_line: *start,
end_line: line_no,
region_type: RegionType::Generated,
content: std::mem::take(buf),
};
regions.push(region);
unannotated_start = line_no + 1;
state = State::Outside;
}
}
State::InPreserve { start, buf } => {
if line.trim_start().starts_with(PRESERVE_START) {
return Err(MergeError::ParseError(format!(
"line {}: nested @ggen:preserve-start (outer started at line {}); \
nested preserve regions are not supported",
line_no, start
)));
}
buf.push_str(line);
buf.push('\n');
if line.trim_start().starts_with(PRESERVE_END) {
let region = MergeRegion {
start_line: *start,
end_line: line_no,
region_type: RegionType::Custom,
content: std::mem::take(buf),
};
regions.push(region);
unannotated_start = line_no + 1;
state = State::Outside;
}
}
}
}
match state {
State::Outside => {
if !unannotated_buf.is_empty() {
regions.push(MergeRegion {
start_line: unannotated_start,
end_line: lines.len(),
region_type: RegionType::Generated,
content: unannotated_buf,
});
}
}
State::InGenerated { start, .. } => {
return Err(MergeError::ParseError(format!(
"unterminated @ggen:generated-start at line {} — missing @ggen:generated-end",
start
)));
}
State::InPreserve { start, .. } => {
return Err(MergeError::ParseError(format!(
"unterminated @ggen:preserve-start at line {} — missing @ggen:preserve-end",
start
)));
}
}
Ok(regions)
}
pub fn three_way_merge(base: &str, ours: &str, theirs: &str) -> Result<MergeResult, MergeError> {
let base_regions = parse_regions(base)?;
let ours_regions = parse_regions(ours)?;
let theirs_regions = parse_regions(theirs)?;
let mut merged = String::new();
let mut conflicts: Vec<MergeConflict> = Vec::new();
let mut preserved_regions = 0usize;
let mut regenerated_regions = 0usize;
let max_len = base_regions
.len()
.max(ours_regions.len())
.max(theirs_regions.len());
for i in 0..max_len {
let base_r = base_regions.get(i);
let ours_r = ours_regions.get(i);
let theirs_r = theirs_regions.get(i);
match (base_r, ours_r, theirs_r) {
(Some(b), Some(o), Some(t)) => {
merge_region_triple(
b,
o,
t,
&mut merged,
&mut conflicts,
&mut preserved_regions,
&mut regenerated_regions,
);
}
(Some(b), None, Some(t)) => {
if t.region_type == RegionType::Custom {
conflicts.push(MergeConflict {
line: b.start_line,
region: "missing-in-ours".to_string(),
base_content: b.content.clone(),
ours: String::new(),
theirs: t.content.clone(),
});
merged.push_str(&format_single_conflict(
b.start_line,
"missing-in-ours",
&b.content,
"",
&t.content,
));
} else {
merged.push_str(&t.content);
regenerated_regions += 1;
}
}
(Some(b), Some(o), None) => {
if o.region_type == RegionType::Custom {
merged.push_str(&o.content);
preserved_regions += 1;
} else if b.content == o.content {
} else {
conflicts.push(MergeConflict {
line: o.start_line,
region: "deleted-in-theirs".to_string(),
base_content: b.content.clone(),
ours: o.content.clone(),
theirs: String::new(),
});
merged.push_str(&format_single_conflict(
o.start_line,
"deleted-in-theirs",
&b.content,
&o.content,
"",
));
}
}
(None, None, Some(t)) => {
merged.push_str(&t.content);
if t.region_type == RegionType::Generated {
regenerated_regions += 1;
}
}
(None, Some(o), None) => {
if o.region_type == RegionType::Custom {
merged.push_str(&o.content);
preserved_regions += 1;
}
}
_ => {
}
}
}
Ok(MergeResult {
merged_content: merged,
conflicts,
preserved_regions,
regenerated_regions,
})
}
fn merge_region_triple(
base: &MergeRegion, ours: &MergeRegion, theirs: &MergeRegion, merged: &mut String,
conflicts: &mut Vec<MergeConflict>, preserved_regions: &mut usize,
regenerated_regions: &mut usize,
) {
match &theirs.region_type {
RegionType::Generated => {
merged.push_str(&theirs.content);
*regenerated_regions += 1;
}
RegionType::Custom => {
merged.push_str(&ours.content);
*preserved_regions += 1;
}
RegionType::Conflict => {
merged.push_str(&format_single_conflict(
theirs.start_line,
"conflict-region",
&base.content,
&ours.content,
&theirs.content,
));
}
}
if theirs.region_type == RegionType::Generated {
let ours_changed = ours.content != base.content;
let theirs_changed = theirs.content != base.content;
if ours_changed && theirs_changed {
let appended_len = theirs.content.len();
let new_len = merged.len() - appended_len;
merged.truncate(new_len);
*regenerated_regions = regenerated_regions.saturating_sub(1);
let label = region_label(theirs);
let conflict = MergeConflict {
line: theirs.start_line,
region: label.clone(),
base_content: base.content.clone(),
ours: ours.content.clone(),
theirs: theirs.content.clone(),
};
merged.push_str(&format_single_conflict(
theirs.start_line,
&label,
&base.content,
&ours.content,
&theirs.content,
));
conflicts.push(conflict);
} else if ours_changed && !theirs_changed {
let appended_len = theirs.content.len();
let new_len = merged.len() - appended_len;
merged.truncate(new_len);
*regenerated_regions = regenerated_regions.saturating_sub(1);
merged.push_str(&ours.content);
}
}
}
fn region_label(r: &MergeRegion) -> String {
let first_line = r.content.lines().next().unwrap_or("");
if first_line.contains(GENERATED_START) {
"generated-region".to_string()
} else if first_line.contains(PRESERVE_START) {
"preserve-region".to_string()
} else {
format!("unannotated-line-{}", r.start_line)
}
}
pub fn apply_generated(
existing_file: &str, new_generated: &str,
) -> Result<MergeResult, MergeError> {
let ours_regions = parse_regions(existing_file)?;
let theirs_regions = parse_regions(new_generated)?;
let mut merged = String::new();
let mut conflicts: Vec<MergeConflict> = Vec::new();
let mut preserved_regions = 0usize;
let mut regenerated_regions = 0usize;
let ours_preserve: Vec<&MergeRegion> = ours_regions
.iter()
.filter(|r| r.region_type == RegionType::Custom)
.collect();
let mut preserve_idx = 0usize;
for theirs_region in &theirs_regions {
match theirs_region.region_type {
RegionType::Generated => {
merged.push_str(&theirs_region.content);
regenerated_regions += 1;
}
RegionType::Custom => {
if let Some(ours_custom) = ours_preserve.get(preserve_idx) {
assert!(
ours_custom.region_type == RegionType::Custom,
"BUG: apply_generated attempted to overwrite a preserve region at line {}; \
this is a logic error — crashing hard to prevent data loss",
ours_custom.start_line
);
merged.push_str(&ours_custom.content);
preserved_regions += 1;
preserve_idx += 1;
} else {
merged.push_str(&theirs_region.content);
preserved_regions += 1;
}
}
RegionType::Conflict => {
merged.push_str(&theirs_region.content);
}
}
}
while preserve_idx < ours_preserve.len() {
let orphan = ours_preserve[preserve_idx];
let conflict = MergeConflict {
line: orphan.start_line,
region: "orphaned-preserve".to_string(),
base_content: String::new(),
ours: orphan.content.clone(),
theirs: String::new(),
};
merged.push_str(&format_single_conflict(
orphan.start_line,
"orphaned-preserve",
"",
&orphan.content,
"",
));
conflicts.push(conflict);
preserve_idx += 1;
}
let output_regions = parse_regions(&merged).unwrap_or_default();
let output_preserve_count = output_regions
.iter()
.filter(|r| r.region_type == RegionType::Custom)
.count();
let input_preserve_count = ours_preserve.len();
if output_preserve_count < input_preserve_count && conflicts.is_empty() {
panic!(
"apply_generated: INVARIANT VIOLATED — {} preserve region(s) in input but only {} \
in output, and no conflict markers were emitted. Crashing to prevent silent data \
loss. Check for bugs in the merge logic.",
input_preserve_count, output_preserve_count
);
}
Ok(MergeResult {
merged_content: merged,
conflicts,
preserved_regions,
regenerated_regions,
})
}
pub fn format_conflicts(conflicts: &[MergeConflict]) -> String {
conflicts
.iter()
.map(|c| format_single_conflict(c.line, &c.region, &c.base_content, &c.ours, &c.theirs))
.collect::<Vec<_>>()
.join("\n")
}
fn format_single_conflict(
line: usize, region: &str, base: &str, ours: &str, theirs: &str,
) -> String {
let mut out = String::new();
out.push_str(&format!(
"<<<<<<< ours (line {}, region: {})\n",
line, region
));
out.push_str(ours);
if !ours.ends_with('\n') {
out.push('\n');
}
out.push_str("||||||| base\n");
out.push_str(base);
if !base.ends_with('\n') {
out.push('\n');
}
out.push_str("=======\n");
out.push_str(theirs);
if !theirs.ends_with('\n') {
out.push('\n');
}
out.push_str(">>>>>>> theirs (regenerated)\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
fn gen_block(body: &str) -> String {
format!("{}\n{}{}\n", GENERATED_START, body, GENERATED_END)
}
fn preserve_block(body: &str) -> String {
format!("{}\n{}{}\n", PRESERVE_START, body, PRESERVE_END)
}
#[test]
fn test_parse_regions_finds_preserve_markers() {
let content = format!(
"package main\n\n{}\n",
preserve_block("// hand-written business logic\nfunc bizLogic() { return 42 }\n")
);
let regions = parse_regions(&content).expect("parse should succeed");
let custom: Vec<_> = regions
.iter()
.filter(|r| r.region_type == RegionType::Custom)
.collect();
assert_eq!(custom.len(), 1, "expected exactly one preserve region");
assert!(
custom[0].content.contains("hand-written business logic"),
"preserve region should contain the hand-written body"
);
}
#[test]
fn test_generated_region_is_overwritten() {
let base = gen_block("// old generated\nfunc OldHandler() {}\n");
let ours = gen_block("// old generated\nfunc OldHandler() {}\n"); let theirs = gen_block("// new generated\nfunc NewHandler() {}\n");
let result = three_way_merge(&base, &ours, &theirs).expect("merge should succeed");
assert!(
result.merged_content.contains("NewHandler"),
"generated region should be overwritten with new content"
);
assert!(
!result.merged_content.contains("OldHandler"),
"old generated handler should be gone"
);
assert_eq!(result.regenerated_regions, 1);
assert_eq!(result.preserved_regions, 0);
}
#[test]
fn test_preserve_region_survives_regeneration() {
let custom_body =
"// CUSTOM: domain-specific validation\nfunc Validate(x int) bool { return x > 0 }\n";
let gen_body_old = "// generated scaffold v1\nfunc Scaffold() {}\n";
let gen_body_new = "// generated scaffold v2\nfunc ScaffoldV2() {}\n";
let base = format!("{}{}", gen_block(gen_body_old), preserve_block(custom_body));
let ours = format!("{}{}", gen_block(gen_body_old), preserve_block(custom_body));
let theirs = format!("{}{}", gen_block(gen_body_new), preserve_block(custom_body));
let result = three_way_merge(&base, &ours, &theirs).expect("merge should succeed");
assert!(
result.merged_content.contains("domain-specific validation"),
"preserve region body must survive"
);
assert!(
result.merged_content.contains("ScaffoldV2"),
"generated region should be updated"
);
assert_eq!(result.preserved_regions, 1, "one preserve region tracked");
assert_eq!(
result.regenerated_regions, 1,
"one generated region tracked"
);
assert!(result.conflicts.is_empty(), "clean merge — no conflicts");
}
#[test]
fn test_conflict_detected_when_both_change() {
let base = "package main\n\nfunc main() { /* base */ }\n";
let ours = "package main\n\nfunc main() { /* hand-edited */ }\n";
let theirs = "package main\n\nfunc main() { /* regenerated */ }\n";
let result = three_way_merge(base, ours, theirs).expect("merge should succeed");
assert!(
!result.conflicts.is_empty(),
"should detect conflict when both ours and theirs differ from base"
);
assert!(
result.merged_content.contains("<<<<<<<"),
"conflict markers should appear in merged output"
);
}
#[test]
fn test_clean_merge_no_annotations_ours_wins() {
let base = "line1\nline2\nline3\n";
let ours = "line1\nline2_edited\nline3\n"; let theirs = "line1\nline2\nline3\n";
let result = three_way_merge(base, ours, theirs).expect("merge should succeed");
assert!(
result.merged_content.contains("line2_edited"),
"when only ours changed, ours should win"
);
assert!(
result.conflicts.is_empty(),
"no conflict — only one side changed"
);
}
#[test]
fn test_three_way_merge_no_changes_returns_theirs() {
let content = gen_block("// no change\nfunc Stable() {}\n");
let result = three_way_merge(&content, &content, &content).expect("merge should succeed");
assert_eq!(
result.merged_content, content,
"identical inputs should produce identical output"
);
assert!(result.conflicts.is_empty());
assert_eq!(result.regenerated_regions, 1);
}
#[test]
fn test_format_conflicts_produces_markers() {
let conflicts = vec![MergeConflict {
line: 10,
region: "unannotated-line-10".to_string(),
base_content: "base content\n".to_string(),
ours: "our content\n".to_string(),
theirs: "their content\n".to_string(),
}];
let formatted = format_conflicts(&conflicts);
assert!(formatted.contains("<<<<<<<"), "should have opening marker");
assert!(
formatted.contains("======="),
"should have separator marker"
);
assert!(formatted.contains(">>>>>>>"), "should have closing marker");
assert!(
formatted.contains("our content"),
"ours should be in markers"
);
assert!(
formatted.contains("their content"),
"theirs should be in markers"
);
assert!(
formatted.contains("base content"),
"base should be in markers"
);
assert!(
formatted.contains("line 10"),
"line number should appear in marker header"
);
}
#[test]
fn test_apply_generated_preserves_custom_business_logic() {
let business_logic = "// IMPORTANT: calculates ARR with churn adjustment\n\
func CalcARR(mrr float64, churn float64) float64 {\n\
\treturn mrr * 12 * (1.0 - churn)\n\
}\n";
let existing = format!(
"package revenue\n\n{}\n{}\n",
gen_block("// v1 scaffold\nfunc Init() {}\n"),
preserve_block(business_logic)
);
let new_generated = format!(
"package revenue\n\n{}\n{}\n",
gen_block("// v2 scaffold — new fields added\nfunc Init() { setup() }\n"),
preserve_block("// placeholder — engineer fills this in\n")
);
let result =
apply_generated(&existing, &new_generated).expect("apply_generated should succeed");
assert!(
result.merged_content.contains("calculates ARR"),
"business logic comment must survive"
);
assert!(
result.merged_content.contains("CalcARR"),
"CalcARR function must survive"
);
assert!(
result.merged_content.contains("new fields added"),
"updated scaffold should appear"
);
assert_eq!(result.preserved_regions, 1, "one preserve region tracked");
assert!(result.conflicts.is_empty(), "no conflicts expected");
}
#[test]
fn test_nested_preserve_regions_not_supported() {
let content = format!(
"{}\n inner block\n {}\n nested content\n {}\nouter end\n{}\n",
PRESERVE_START, PRESERVE_START, PRESERVE_END, PRESERVE_END
);
let result = parse_regions(&content);
assert!(
result.is_err(),
"nested preserve regions should produce a ParseError"
);
match result {
Err(MergeError::ParseError(msg)) => {
assert!(
msg.contains("nested"),
"error message should mention 'nested': {msg}"
);
}
_ => panic!("expected MergeError::ParseError"),
}
}
#[test]
fn test_empty_file_merge() {
let result = three_way_merge("", "", "").expect("empty merge should succeed");
assert_eq!(
result.merged_content, "",
"empty inputs produce empty output"
);
assert!(result.conflicts.is_empty());
assert_eq!(result.preserved_regions, 0);
assert_eq!(result.regenerated_regions, 0);
}
#[test]
fn test_apply_generated_no_preserve_regions_pure_overwrite() {
let existing = gen_block("// old scaffold\nfunc OldSetup() {}\n");
let new_generated = gen_block("// new scaffold\nfunc NewSetup() {}\n");
let result =
apply_generated(&existing, &new_generated).expect("apply_generated should succeed");
assert!(
result.merged_content.contains("NewSetup"),
"new scaffold must be present"
);
assert!(
!result.merged_content.contains("OldSetup"),
"old scaffold must be gone"
);
assert_eq!(result.preserved_regions, 0);
assert_eq!(result.regenerated_regions, 1);
}
#[test]
fn test_multiple_preserve_regions_all_survive() {
let logic_a = "func BusinessRuleA() bool { return true }\n";
let logic_b = "func BusinessRuleB() bool { return false }\n";
let existing = format!(
"{}{}{}\n",
preserve_block(logic_a),
gen_block("// v1 glue\n"),
preserve_block(logic_b)
);
let new_gen = format!(
"{}{}{}\n",
preserve_block("// placeholder A\n"),
gen_block("// v2 glue — updated\n"),
preserve_block("// placeholder B\n")
);
let result = apply_generated(&existing, &new_gen).expect("apply_generated should succeed");
assert!(
result.merged_content.contains("BusinessRuleA"),
"first preserve block must survive"
);
assert!(
result.merged_content.contains("BusinessRuleB"),
"second preserve block must survive"
);
assert!(
result.merged_content.contains("v2 glue"),
"updated generated content must appear"
);
assert_eq!(
result.preserved_regions, 2,
"two preserve regions must be tracked"
);
assert!(
result.regenerated_regions >= 1,
"at least the explicit generated block should be counted"
);
}
#[test]
fn test_unannotated_theirs_only_change_theirs_wins() {
let base = "package main\n\nconst Version = \"1.0\"\n";
let ours = base; let theirs = "package main\n\nconst Version = \"2.0\"\n";
let result = three_way_merge(base, ours, theirs).expect("merge should succeed");
assert!(
result.merged_content.contains("\"2.0\""),
"theirs version bump should be applied"
);
assert!(
result.conflicts.is_empty(),
"no conflict — only theirs changed"
);
}
#[test]
fn test_conflict_markers_contain_line_number() {
let base = "a\nb\nc\n";
let ours = "a\nX\nc\n";
let theirs = "a\nY\nc\n";
let result = three_way_merge(base, ours, theirs).expect("merge should succeed");
assert!(!result.conflicts.is_empty(), "conflict should be detected");
let line_no = result.conflicts[0].line;
assert!(
result.merged_content.contains(&format!("line {}", line_no)),
"merged output conflict marker should contain the line number"
);
}
#[test]
fn test_unterminated_generated_block_is_parse_error() {
let content = format!("{}\nsome content\n", GENERATED_START);
let result = parse_regions(&content);
assert!(
result.is_err(),
"unterminated generated block should be a ParseError"
);
match result {
Err(MergeError::ParseError(msg)) => {
assert!(
msg.contains("unterminated"),
"error should say 'unterminated': {msg}"
);
}
_ => panic!("expected MergeError::ParseError"),
}
}
}