69 """Structured output from :func:`run_migrate_option_c`."""
71 copied: list[tuple[str, Path, Path]] = field(default_factory=list)
72 skipped: list[tuple[str, str]] = field(default_factory=list)
73 backed_up: list[Path] = field(default_factory=list)
74 messages: list[str] = field(default_factory=list)
75 stage: str =
"bootstrap"
78_ARTEFACT_SPEC: list[tuple[str, str, list[str], str]] = [
80 (
"octrc",
"octrc.json", [
".octrc.json"],
"file"),
84 [
"oc_diagnostics/debug_config.json",
"diagnostics/debug_config.json"],
87 (
"logs",
"logs", [
"logs"],
"dir"),
94 dry_run: bool =
False,
95 no_backup: bool =
False,
97 """Migrate a legacy Option C project into the ``.option_c/`` layout.
99 Returns a :class:`MigrationResult` describing what was copied,
100 skipped, or backed up. The caller renders the result.
102 func =
"run_migrate_option_c"
103 _dbg(
"MIGRATE", func, f
"start root={project_root} dry_run={dry_run}", 2)
109 result.messages.append(f
"[DRY RUN] Migration target: {oc_dir}")
110 for label, dest_rel, source_candidates, kind
in _ARTEFACT_SPEC:
113 result.messages.append(f
"[DRY RUN] {label}: not found - skip")
114 result.skipped.append((label,
"not found"))
116 dest = oc_dir / dest_rel
118 result.messages.append(
119 f
"[DRY RUN] {label}: {src} -> {dest} (destination exists, "
120 f
"{'overwrite' if no_backup else 'backup + overwrite'})"
123 result.messages.append(
124 f
"[DRY RUN] {label}: {src} -> {dest}"
126 result.messages.append(
127 f
"[DRY RUN] Would write {oc_dir / 'oc_status.json'}"
129 result.messages.append(
"[DRY RUN] Migration plan complete (no files written)")
130 _dbg(
"MIGRATE", func,
"dry-run end", 2)
133 oc_dir.mkdir(parents=
True, exist_ok=
True)
134 (oc_dir /
"cache").mkdir(exist_ok=
True)
136 migrated_from: dict[str, str] = {}
137 pillars_found: set[str] = set()
139 for label, dest_rel, source_candidates, kind
in _ARTEFACT_SPEC:
142 result.skipped.append((label,
"not found"))
143 _dbg(
"MIGRATE", func, f
"{label}: no source found, skipping", 3)
146 dest = oc_dir / dest_rel
150 result.skipped.append((label,
"already migrated"))
151 result.messages.append(
152 f
" {label}: already at {dest} (identical) - skip"
154 _dbg(
"MIGRATE", func, f
"{label}: dest identical, skip", 3)
155 pillars_found.add(label)
156 migrated_from[label] = str(
157 src.relative_to(project_root)
161 backup = Path(str(dest) +
".bk")
164 shutil.rmtree(backup)
165 shutil.copytree(dest, backup)
167 shutil.copy2(dest, backup)
168 result.backed_up.append(backup)
169 result.messages.append(f
" {label}: backed up {dest} -> {backup}")
174 shutil.copytree(src, dest)
176 shutil.copy2(src, dest)
178 result.copied.append((label, src, dest))
179 result.messages.append(f
" {label}: {src} -> {dest}")
180 pillars_found.add(label)
181 migrated_from[label] = src.relative_to(project_root).as_posix()
182 _dbg(
"MIGRATE", func, f
"{label}: copied {src} -> {dest}", 3)
184 _pillar_label_to_name = {
186 "debug_config":
"diagnostics",
189 pillar_names_found = {
190 _pillar_label_to_name.get(p, p)
for p
in pillars_found
192 all_pillars = {
"linter",
"diagnostics",
"git",
"docs",
"tests",
"scaffold"}
193 stage =
"compliant" if pillar_names_found
else "bootstrap"
194 if pillar_names_found
and pillar_names_found != all_pillars:
199 from importlib.metadata
import PackageNotFoundError, version
201 oct_version = version(
"oct")
202 except PackageNotFoundError:
203 oct_version =
"unknown"
205 oct_version =
"unknown"
208 today = datetime.date.today().isoformat()
210 for name
in (
"linter",
"diagnostics",
"git",
"docs",
"tests",
"scaffold"):
211 pillars_dict[name] = {
212 "enabled": name
in pillar_names_found,
213 "since": today
if name
in pillar_names_found
else None,
218 pillars=pillars_dict,
219 migrated_from=migrated_from,
220 oct_version_at_migration=oct_version,
222 write_oc_status(project_root, status)
223 result.messages.append(f
" oc_status.json: stage={stage}")
227 f
"end copied={len(result.copied)} skipped={len(result.skipped)} "
270 ctx: click.Context, dry_run: bool, no_backup: bool,
272 """Migrate to the canonical .option_c/ dotdir layout (FS-539)."""
273 project_root = get_project_root(ctx)
275 project_root, dry_run=dry_run, no_backup=no_backup,
277 for msg
in result.messages:
279 if not dry_run
and not result.copied
and not any(
280 reason ==
"already migrated" for _, reason
in result.skipped
282 click.echo(
"Nothing to migrate - no legacy artefacts found.")