1717import json
1818import logging
1919import os
20- import re
2120import shutil
2221import subprocess
2322from typing import Optional
@@ -113,7 +112,7 @@ def reorder_param_files(steps: dict) -> dict[str, str]:
113112 return renames
114113
115114
116- def loop_relevant_files (renames : dict [str , str ], steps : dict ) -> list [str ]:
115+ def loop_relevant_files (renames : dict [str , str ]) -> list [str ]:
117116 param_dirs = ["." ]
118117 # Search all *.py, *.json and *.md files in the current directory
119118 # and replace all occurrences of the old names with the new names
@@ -125,76 +124,109 @@ def loop_relevant_files(renames: dict[str, str], steps: dict) -> list[str]:
125124 continue
126125 if file == "vehicle_components.json" :
127126 continue
128- if file == SEQUENCE_FILENAME :
129- uplate_old_filenames (renames , steps )
130127 if file in PYTHON_FILES or file .endswith ((".md" , ".json" )):
131- update_file_contents (renames , root , file , steps )
128+ update_file_contents (renames , root , file )
132129 return param_dirs
133130
134131
135- def uplate_old_filenames (renames : dict [str , str ], steps : dict ) -> None :
136- for new_name , old_name in renames .items ():
137- if old_name != new_name :
138- if "old_filenames" in steps [old_name ]:
139- if old_name not in steps [old_name ]["old_filenames" ]:
140- steps [old_name ]["old_filenames" ].append (old_name )
141- else :
142- steps [old_name ]["old_filenames" ] = [old_name ]
143-
144-
145- def update_file_contents (renames : dict [str , str ], root : str , file : str , steps : dict ) -> None :
132+ def update_file_contents (renames : dict [str , str ], root : str , file : str ) -> None :
146133 with open (os .path .join (root , file ), encoding = "utf-8" , newline = "" ) as handle :
147134 file_content = handle .read ()
148- if file .startswith ("TUNING_GUIDE_" ) and file .endswith (".md" ):
149- for old_filename in renames .values ():
150- if old_filename not in file_content :
151- msg = f"The intermediate parameter file '{ old_filename } ' is not mentioned in the { file } file"
152- logging .error (msg )
153- for new_name , old_name in renames .items ():
154- if "configuration_steps" in file and file .endswith (".json" ):
155- file_content = update_configuration_steps_json_file_contents (steps , file_content , new_name , old_name )
156- else :
135+ if "configuration_steps" in file and file .endswith (".json" ):
136+ # Use renames (current JSON key -> new key) so that historical names already
137+ # stored in old_filenames are never accidentally overwritten.
138+ for new_name , old_name in renames .items ():
139+ file_content = file_content .replace (old_name , new_name )
140+ else :
141+ if file .startswith ("TUNING_GUIDE_" ) and file .endswith (".md" ):
142+ for old_filename in file_renames :
143+ if old_filename not in file_content :
144+ msg = f"The intermediate parameter file '{ old_filename } ' is not mentioned in the { file } file"
145+ logging .error (msg )
146+ for old_name , new_name in file_renames .items ():
157147 file_content = file_content .replace (old_name , new_name )
158148 with open (os .path .join (root , file ), "w" , encoding = "utf-8" , newline = "" ) as handle :
159149 handle .write (file_content )
160150
161151
162- def update_configuration_steps_json_file_contents (steps : dict , file_content : str , new_name : str , old_name : str ) -> str :
163- new_file_content = ""
164- curr_filename = ""
165- in_steps_block = False
152+ def _parse_old_filenames_from_line (line : str ) -> list [str ]:
153+ """Extract filename strings from an old_filenames JSON line."""
154+ bracket_start = line .index ("[" )
155+ bracket_end = line .rindex ("]" )
156+ text = line [bracket_start + 1 : bracket_end ].strip ()
157+ return [v .strip ().strip ('"' ) for v in text .split ("," )] if text else []
166158
167- for line in file_content .splitlines (keepends = True ):
168- # Track when we enter/exit the "steps" block
169- if '"steps": {' in line :
170- in_steps_block = True
171- new_file_content += line
172- continue
173159
174- # Only process filenames when inside steps block
175- if in_steps_block :
176- re_search = re .search (r"^ \"(\w.+)\"" , line )
177- if re_search :
178- curr_filename = re_search .group (1 )
179-
180- if "old_filenames" in line :
181- if curr_filename in steps and "old_filenames" in steps [curr_filename ]:
182- # WARNING!!! old_filenames can only used once, so we remove it after using it
183- old_filenames = str (steps [curr_filename ].pop ("old_filenames" )).replace ("'" , '"' )
184- new_file_content += f' "old_filenames": { old_filenames } '
185- if line .endswith (",\n " ):
186- new_file_content += ","
187- new_file_content += "\n "
188- else :
189- new_file_content += line
190- else :
191- new_file_content += line .replace (old_name , new_name )
160+ def _format_old_filenames_line (indent : str , values : list [str ], trailing_comma : bool ) -> str :
161+ """Format an old_filenames JSON line."""
162+ values_str = ", " .join (f'"{ v } "' for v in values )
163+ return f'{ indent } "old_filenames": [{ values_str } ]{ "," if trailing_comma else "" } \n '
192164
193- # Track end of steps block
194- if in_steps_block and line .strip () == "}," and curr_filename :
195- in_steps_block = False
196165
197- return new_file_content
166+ def update_old_filenames_in_json_file (json_path : str ) -> None :
167+ """
168+ Update old_filenames fields in the JSON file using targeted text replacement.
169+
170+ Must be called after loop_relevant_files so the JSON already has the new step keys.
171+ Reads the file as text to avoid reformatting. Uses brace counting to correctly
172+ identify step block boundaries and deduplicates multiple old_filenames entries.
173+ """
174+ with open (json_path , encoding = "utf-8" , newline = "" ) as f :
175+ lines = f .readlines ()
176+
177+ needs_update : dict [str , list [str ]] = {}
178+ for old_name , new_name in file_renames .items ():
179+ if old_name != new_name :
180+ needs_update .setdefault (new_name , []).append (old_name )
181+
182+ new_lines : list [str ] = []
183+ i = 0
184+ while i < len (lines ):
185+ line = lines [i ]
186+ # Step keys have exactly 8 spaces of indentation inside "steps": { ... }
187+ if not (line .startswith (' "' ) and line .rstrip ().endswith ('": {' )):
188+ new_lines .append (line )
189+ i += 1
190+ continue
191+ stripped = line .strip ()
192+ step_key = stripped [1 : stripped .index ('"' , 1 )]
193+ if step_key not in needs_update :
194+ new_lines .append (line )
195+ i += 1
196+ continue
197+ # Collect all lines for this step using brace counting
198+ step_lines : list [str ] = [line ]
199+ i += 1
200+ depth = 1
201+ while i < len (lines ) and depth > 0 :
202+ depth += lines [i ].count ("{" ) - lines [i ].count ("}" )
203+ step_lines .append (lines [i ])
204+ i += 1
205+ # Merge all old_filenames entries (existing + new names) into one
206+ of_indices = [k for k , sl in enumerate (step_lines ) if sl .strip ().startswith ('"old_filenames"' )]
207+ merged : list [str ] = []
208+ for k in of_indices :
209+ for v in _parse_old_filenames_from_line (step_lines [k ]):
210+ if v not in merged :
211+ merged .append (v )
212+ for name in needs_update [step_key ]:
213+ if name not in merged :
214+ merged .append (name )
215+ if of_indices :
216+ first = of_indices [0 ]
217+ first_line = step_lines [first ]
218+ indent = first_line [: len (first_line ) - len (first_line .lstrip ())]
219+ step_lines [first ] = _format_old_filenames_line (indent , merged , first_line .rstrip ().endswith ("," ))
220+ for k in reversed (of_indices [1 :]):
221+ del step_lines [k ]
222+ else :
223+ ref = step_lines [1 ] if len (step_lines ) > 1 else ""
224+ indent = ref [: len (ref ) - len (ref .lstrip ())] if ref .strip () else " "
225+ step_lines .insert (1 , _format_old_filenames_line (indent , merged , trailing_comma = True ))
226+ new_lines .extend (step_lines )
227+
228+ with open (json_path , "w" , encoding = "utf-8" , newline = "" ) as f :
229+ f .writelines (new_lines )
198230
199231
200232def _git_executable () -> Optional [str ]:
@@ -238,18 +270,20 @@ def rename_file(old_name: str, new_name: str, param_dir: str) -> None:
238270 if git_executable and _is_git_tracked (old_name_path ):
239271 try :
240272 subprocess .run ( # noqa: S603
241- [git_executable , "mv" , "--" , old_name_path , new_name_path ],
273+ [git_executable , "mv" , "-f" , "- -" , old_name_path , new_name_path ],
242274 check = True ,
243275 capture_output = True ,
244276 text = True ,
245277 )
246278 return
247279 except subprocess .CalledProcessError as exc :
280+ stderr = exc .stderr .strip () if exc .stderr else ""
248281 logging .warning (
249- "git mv failed for %s -> %s, falling back to os.rename: %s" ,
282+ "git mv failed for %s -> %s, falling back to os.rename: %s%s " ,
250283 old_name_path ,
251284 new_name_path ,
252285 exc ,
286+ f"\n stderr: { stderr } " if stderr else "" ,
253287 )
254288 os .rename (old_name_path , new_name_path )
255289
@@ -285,12 +319,14 @@ def change_line_endings_for_md_files() -> None:
285319
286320def main () -> None :
287321 logging .basicConfig (level = "INFO" , format = "%(asctime)s - %(levelname)s - %(message)s" )
288- with open (os .path .join ("ardupilot_methodic_configurator" , SEQUENCE_FILENAME ), encoding = "utf-8" ) as f :
322+ json_path = os .path .join ("ardupilot_methodic_configurator" , SEQUENCE_FILENAME )
323+ with open (json_path , encoding = "utf-8" ) as f :
289324 json_content = json .load (f )
290325 steps = json_content ["steps" ]
291326 renames = reorder_param_files (steps )
292- param_dirs = loop_relevant_files (renames , steps )
327+ param_dirs = loop_relevant_files (renames )
293328 reorder_actual_files (renames , param_dirs )
329+ update_old_filenames_in_json_file (json_path )
294330 # change_line_endings_for_md_files()
295331
296332
0 commit comments