{"record":{"id":"54f23fab47734aba","repo":"gitbutlerapp/gitbutler","slug":"ownership-ranges-cannot-be-empty","errorCode":null,"errorMessage":"ownership ranges cannot be empty","messagePattern":"ownership ranges cannot be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-meta/src/virtual_branches_legacy_types.rs","lineNumber":335,"sourceCode":"\n        fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {\n            let mut file_path_parts = vec![];\n            let mut ranges = vec![];\n            for part in value.split(':').rev() {\n                match part\n                    .split(',')\n                    .map(str::parse)\n                    .collect::<anyhow::Result<Vec<Hunk>>>()\n                {\n                    Ok(rr) => ranges.extend(rr),\n                    Err(_) => {\n                        file_path_parts.insert(0, part);\n                    }\n                }\n            }\n\n            if ranges.is_empty() {\n                Err(anyhow::anyhow!(\"ownership ranges cannot be empty\"))\n            } else {\n                Ok(Self {\n                    file_path: file_path_parts\n                        .join(\":\")\n                        .parse()\n                        .context(format!(\"failed to parse file path from {value}\"))?,\n                    hunks: ranges.clone(),\n                })\n            }\n        }\n    }\n\n    impl fmt::Display for OwnershipClaim {\n        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {\n            if self.hunks.is_empty() {\n                write!(f, \"{}\", self.file_path.display())\n            } else {\n                write!(","sourceCodeStart":317,"sourceCodeEnd":353,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-meta/src/virtual_branches_legacy_types.rs#L317-L353","documentation":"Thrown by OwnershipClaim::from_str in crates/but-meta/src/virtual_branches_legacy_types.rs when parsing a BranchOwnershipClaims line: the code splits the value on ':' from the right and tries to parse each segment as a comma-separated list of Hunk ranges; if no segment parses, ranges stays empty and the claim is rejected because an ownership claim must own at least one hunk range. The expected line format is a file path followed by one or more ':'-separated hunk specs, e.g. \"src/main.rs:1-10,20-30\".","triggerScenarios":"Parsing a claims line like \"src/main.rs\" with no range segment; ranges written in an unparseable form (e.g. \"1_10\" or \"1..10\" instead of \"1-10\", or non-numeric bounds); a Windows path where every segment fails Hunk::parse so nothing lands in ranges.","commonSituations":"Hand-edited or machine-generated virtual_branches.json ownership claims missing the hunk suffix; code writing claims with a different separator than Display uses; legacy data from tools that emitted path-only claims.","solutions":["Give every claim line at least one valid hunk range: \"path/to/file.rs:<start>-<end>\" (comma-separate multiple ranges).","Check the range syntax uses '-' between numeric bounds — '..' and '_' do not parse as Hunks.","Regenerate the claims by re-assigning hunks in the app rather than hand-writing them, so Display/FromStr stay symmetric.","If migrating old data, preprocess path-only lines by appending the full-file range or dropping them before parsing."],"exampleFix":"// before: path-only claim, parses to zero ranges\nlet claim: anyhow::Result<OwnershipClaim> = \"src/main.rs\".parse(); // Err: ownership ranges cannot be empty\n\n// after: path plus hunk range\nlet claim: anyhow::Result<OwnershipClaim> = \"src/main.rs:1-42\".parse(); // Ok","handlingStrategy":"validation","validationCode":"// Validate a claims line before parsing: needs path + at least one N-N range segment\nfn claim_line_is_valid(line: &str) -> bool {\n    let colon = line.find(':');\n    if colon.is_none() { return false; }\n    line[colon.unwrap() + 1..]\n        .split(',')\n        .all(|r| r.split_once('-').map(|(a, b)| a.parse::<u32>().is_ok() && b.parse::<u32>().is_ok()).unwrap_or(false))\n}","typeGuard":"fn has_valid_hunk_range(line: &str) -> bool {\n    line.split(':').skip(1).any(|seg| {\n        seg.split(',').all(|r| {\n            r.split_once('-')\n                .map(|(a, b)| a.parse::<u32>().is_ok() && b.parse::<u32>().is_ok())\n                .unwrap_or(false)\n        })\n    })\n}","tryCatchPattern":"match line.parse::<OwnershipClaim>() {\n    Ok(claim) => claims.push(claim),\n    Err(e) if e.to_string().contains(\"ownership ranges cannot be empty\") => {\n        tracing::warn!(\"skipping range-less ownership line: {line}\"); continue;\n    },\n    Err(e) => return Err(e),\n}","preventionTips":["Always serialize ownership claims with Display (path + ':' + ranges) instead of hand-writing strings.","Round-trip test: parse(claim.to_string()) must succeed for every claim you write.","When importing legacy data, drop or repair path-only lines before FromStr parsing."],"tags":["metadata","parsing","ownership-claims","legacy-migration"],"backgroundTag":"ownership-claim-parse-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}