From 84150260e19a82159e05df40d40268493e6049e6 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sat, 4 Jul 2026 16:13:31 -0400 Subject: [PATCH 1/2] Fix ini.set_option deleting indented options In _Section.refresh, an indented line was appended to the previous option's value, but when a section began with indented options (no prior option to append to) the line hit 'continue' and was silently discarded. Move the 'continue' inside the append branch so orphaned indented lines fall through to the normal key/value match and are preserved. Fixes #36354 --- changelog/36354.fixed.md | 1 + salt/modules/ini_manage.py | 2 +- tests/pytests/unit/modules/test_ini_manage.py | 33 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 changelog/36354.fixed.md diff --git a/changelog/36354.fixed.md b/changelog/36354.fixed.md new file mode 100644 index 000000000000..f7135b159d28 --- /dev/null +++ b/changelog/36354.fixed.md @@ -0,0 +1 @@ +ini.set_option now preserves indented options in other sections instead of deleting them. diff --git a/salt/modules/ini_manage.py b/salt/modules/ini_manage.py index 05072ef318d3..9060ce8e35b1 100644 --- a/salt/modules/ini_manage.py +++ b/salt/modules/ini_manage.py @@ -447,7 +447,7 @@ def refresh(self, inicontents=None): prev_opt = options[-1] value = self.get(prev_opt) self.update({prev_opt: os.linesep.join((value, opt_str))}) - continue + continue # Match normal key+value lines. opt_match = self.opt_regx.match(opt_str) if opt_match: diff --git a/tests/pytests/unit/modules/test_ini_manage.py b/tests/pytests/unit/modules/test_ini_manage.py index e226f34dfaac..aa34cee3bf15 100644 --- a/tests/pytests/unit/modules/test_ini_manage.py +++ b/tests/pytests/unit/modules/test_ini_manage.py @@ -520,3 +520,36 @@ def test_unicode_remove_section(encoding, linesep, ini_file, unicode_content): } assert ini.remove_section(str(ini_file), "Юникод", encoding=encoding) == expected assert ini.get_section(str(ini_file), "Юникод", encoding=encoding) == {} + + +def test_set_option_preserves_indented_options(ini_file): + """ + Test that setting an option does not delete indented options in other + sections (e.g. a git-style config where options are indented). + + Regression test for #36354. + """ + ini_content = os.linesep.join( + [ + "[core]", + "", + '[remote "origin"]', + " url = git@version-control:test.git", + " fetch = +refs/heads/*:refs/remotes/origin/*", + ] + ) + ini_file.write_text(ini_content) + + ini.set_option(str(ini_file), {"core": {"sharedRepository": "group"}}) + + # The indented options in the untouched section must survive + assert ( + ini.get_option(str(ini_file), 'remote "origin"', "url") + == "git@version-control:test.git" + ) + assert ( + ini.get_option(str(ini_file), 'remote "origin"', "fetch") + == "+refs/heads/*:refs/remotes/origin/*" + ) + # The new option was still written + assert ini.get_option(str(ini_file), "core", "sharedRepository") == "group" From 4e1034428d391dbf4c44da153d589d16a05abe34 Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Sun, 5 Jul 2026 23:18:06 -0400 Subject: [PATCH 2/2] Add direct and inverse regression tests for ini indented options fix The direct test calls _Section.refresh() itself the way _Ini.refresh does in production (section body passed positionally as inicontents with separator="=", refresh() invoked with no arguments) on a git-style section whose options are all indented, which the old code silently dropped. The inverse test guards against overcorrection by verifying an indented line following a normal option is still folded into that option's value as a continuation line rather than parsed as a separate entry. --- tests/pytests/unit/modules/test_ini_manage.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/pytests/unit/modules/test_ini_manage.py b/tests/pytests/unit/modules/test_ini_manage.py index aa34cee3bf15..cc7c75ee4580 100644 --- a/tests/pytests/unit/modules/test_ini_manage.py +++ b/tests/pytests/unit/modules/test_ini_manage.py @@ -553,3 +553,52 @@ def test_set_option_preserves_indented_options(ini_file): ) # The new option was still written assert ini.get_option(str(ini_file), "core", "sharedRepository") == "group" + + +def test_section_refresh_parses_leading_indented_options_36354(): + """ + Call the fixed _Section.refresh directly with a section body whose + options are all indented (git-style config), the case that used to be + silently dropped. + + Regression test for #36354. + """ + # Mirror the production call site in _Ini.refresh: the section body is + # passed positionally as inicontents with separator="=" and refresh() + # is then called with no arguments, so it parses self.inicontents. + sect_ini = os.linesep.join( + [ + " url = git@version-control:test.git", + " fetch = +refs/heads/*:refs/remotes/origin/*", + ] + ) + sect = ini._Section('remote "origin"', sect_ini, separator="=") + sect.refresh() + + # Before the fix, refresh() consumed indented lines even when there was + # no previous option to append them to, so the section came back empty. + assert sect.get("url") == "git@version-control:test.git" + assert sect.get("fetch") == "+refs/heads/*:refs/remotes/origin/*" + + +def test_section_refresh_keeps_continuation_lines_36354(): + """ + Guard against overcorrection of the #36354 fix: an indented line that + follows a normal option must still be folded into that option's value + as a continuation line, not parsed as a separate option or dropped. + This passes with and without the fix. + """ + sect_ini = os.linesep.join( + [ + "key1 = value1", + " continuation line", + "key2 = value2", + ] + ) + sect = ini._Section("test", sect_ini, separator="=") + sect.refresh() + + assert sect.get("key1") == os.linesep.join(["value1", " continuation line"]) + assert sect.get("key2") == "value2" + # The continuation line must not have become its own entry + assert len(sect) == 2