From 21f4b6937faeb54ea22b5b9742fb5eab49095682 Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Wed, 16 Jul 2025 15:52:05 +0200 Subject: [PATCH 1/3] [Docs] Document TDirectory::Add --- core/base/inc/TDirectory.h | 1 + 1 file changed, 1 insertion(+) diff --git a/core/base/inc/TDirectory.h b/core/base/inc/TDirectory.h index 8be0aa9af9358..9a7e5af175a5c 100644 --- a/core/base/inc/TDirectory.h +++ b/core/base/inc/TDirectory.h @@ -180,6 +180,7 @@ can be replaced with the simpler and exception safe: static void AddDirectory(Bool_t add=kTRUE); static Bool_t AddDirectoryStatus(); virtual void Append(TObject *obj, Bool_t replace = kFALSE); + /// Append object to this directory. \see Append(TObject*, Bool_t) virtual void Add(TObject *obj, Bool_t replace = kFALSE) { Append(obj,replace); } virtual Int_t AppendKey(TKey *) {return 0;} void Browse(TBrowser *b) override; From 2d2d7e5fc552ba627aed2f8c15a50462b102feb7 Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Wed, 9 Jul 2025 19:22:01 +0200 Subject: [PATCH 2/3] [Docs] Make TFileMerger::PartialMerge documentation readable. Doxygen was squashing this block into a paragraph, so it needed to be split off and indented. Also add docstring for AddObjectNames. --- io/io/inc/TFileMerger.h | 1 + io/io/src/TFileMerger.cxx | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/io/io/inc/TFileMerger.h b/io/io/inc/TFileMerger.h index e20a08fd94486..9d1f724de6d58 100644 --- a/io/io/inc/TFileMerger.h +++ b/io/io/inc/TFileMerger.h @@ -109,6 +109,7 @@ class TFileMerger : public TObject { void SetMergeOptions(const TString &options) { fMergeOptions = options; } void SetMergeOptions(const std::string_view &options) { fMergeOptions = options; } void SetIOFeatures(ROOT::TIOFeatures &features) { fIOFeatures = &features; } + /// Add object names for PartialMerge(). void AddObjectNames(const char *name) {fObjectNames += name; fObjectNames += " ";} const char *GetObjectNames() const {return fObjectNames.Data();} void ClearObjectNames() {fObjectNames.Clear();} diff --git a/io/io/src/TFileMerger.cxx b/io/io/src/TFileMerger.cxx index afac10c1ad56b..d349e33876fe9 100644 --- a/io/io/src/TFileMerger.cxx +++ b/io/io/src/TFileMerger.cxx @@ -936,16 +936,17 @@ Bool_t TFileMerger::MergeRecursive(TDirectory *target, TList *sourcelist, Int_t /// the file "FileMerger.root" in the working directory. Returns true /// on success, false in case of error. /// The type is defined by the bit values in EPartialMergeType: -/// kRegular : normal merge, overwritting the output file -/// kIncremental : merge the input file with the content of the output file (if already exising) (default) -/// kResetable : merge only the objects with a MergeAfterReset member function. -/// kNonResetable : merge only the objects without a MergeAfterReset member function. -/// kDelayWrite : delay the TFile write (to reduce the number of write when reusing the file) -/// kAll : merge all type of objects (default) -/// kAllIncremental : merge incrementally all type of objects. -/// kOnlyListed : merge only the objects specified in fObjectNames list -/// kSkipListed : skip objects specified in fObjectNames list -/// kKeepCompression: keep compression level unchanged for each input +/// +/// kRegular : normal merge, overwriting the output file +/// kIncremental : merge the input file with the content of the output file (if already exising) (default) +/// kResetable : merge only the objects with a MergeAfterReset member function. +/// kNonResetable : merge only the objects without a MergeAfterReset member function. +/// kDelayWrite : delay the TFile write (to reduce the number of write when reusing the file) +/// kAll : merge all type of objects (default) +/// kAllIncremental : merge incrementally all type of objects. +/// kOnlyListed : merge only the objects specified in fObjectNames list +/// kSkipListed : skip objects specified in fObjectNames list +/// kKeepCompression: keep compression level unchanged for each input /// /// If the type is not set to kIncremental, the output file is deleted at the end of this operation. From 5f2f367d1ecb13f05bb77e6d4f04aa915497920a Mon Sep 17 00:00:00 2001 From: Stephan Hageboeck Date: Thu, 10 Jul 2025 12:29:49 +0200 Subject: [PATCH 3/3] [IO] Suppress empty directories from TFileMerger's partial merge. Partial merging creates a full directory hierarchy in an output file, even if the user didn't ask for the directories to be merged. Fix #19330. --- io/io/src/TFileMerger.cxx | 11 +++++-- io/io/test/TFileMergerTests.cxx | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/io/io/src/TFileMerger.cxx b/io/io/src/TFileMerger.cxx index d349e33876fe9..0f4ec824f14b5 100644 --- a/io/io/src/TFileMerger.cxx +++ b/io/io/src/TFileMerger.cxx @@ -591,8 +591,15 @@ Bool_t TFileMerger::MergeOne(TDirectory *target, TList *sourcelist, Int_t type, // GetPath(), so we can still figure out where we are in the recursion // If this folder is a onlyListed object, merge everything inside. - if (onlyListed) type &= ~kOnlyListed; - status = MergeRecursive(newdir, sourcelist, type); + const auto mergeType = onlyListed ? type & ~kOnlyListed : type; + status = MergeRecursive(newdir, sourcelist, mergeType); + + if ((type & kOnlyListed) && !(type & kIncremental) && !onlyListed && newdir->GetNkeys() == 0) { + // None of the children were merged, and the directory is not listed + delete newdir; + newdir = nullptr; + target->rmdir(obj->GetName()); + } // Delete newdir directory after having written it (merged) if (!(type&kIncremental)) delete newdir; if (onlyListed) type |= kOnlyListed; diff --git a/io/io/test/TFileMergerTests.cxx b/io/io/test/TFileMergerTests.cxx index 39b785bfe0ceb..e7aecd7833738 100644 --- a/io/io/test/TFileMergerTests.cxx +++ b/io/io/test/TFileMergerTests.cxx @@ -221,3 +221,61 @@ TEST(TFileMerger, ChangeFile) gSystem->Unlink("file6640mergerinput_2.root"); gSystem->Unlink("file6640mergeroutput.root"); } + +TEST(TFileMerger, SelectiveMergeWithDirectories) +{ + constexpr auto input1 = "selectiveMerge_input_1.root"; + constexpr auto input2 = "selectiveMerge_input_2.root"; + constexpr auto output = "selectiveMerge_output.root"; + for (auto const &filename : {input1, input2}) { + TH1F histo("histo", "Histo", 2, 0, 1); + TFile infile(filename, "recreate"); + auto dir = infile.mkdir("A"); + dir->WriteObject(&histo, "Histo_A1"); + if (filename == input1) + dir->WriteObject(&histo, "Histo_A2"); + if (filename == input2) + dir->WriteObject(&histo, "Histo_A3"); + + dir = infile.mkdir("B"); + dir->WriteObject(&histo, "Histo_B"); + + dir = infile.mkdir("C")->mkdir("D"); + dir->WriteObject(&histo, "Histo_D"); + dir = infile.mkdir("E")->mkdir("F"); + dir->WriteObject(&histo, "Histo_F1"); + if (filename == input1) + dir->WriteObject(&histo, "Histo_F2"); + if (filename == input2) + dir->WriteObject(&histo, "Histo_F3"); + } + + { + TFileMerger fileMerger(false); + fileMerger.AddFile(input1); + fileMerger.AddFile(input2); + fileMerger.AddObjectNames("A"); + fileMerger.AddObjectNames("Histo_F1 Histo_F2 Histo_F3"); + fileMerger.OutputFile(output); + fileMerger.PartialMerge(TFileMerger::kOnlyListed | TFileMerger::kAll | TFileMerger::kRegular); + } + + TFile outfile(output); + auto dir = outfile.Get("A"); + ASSERT_NE(dir, nullptr); + for (auto name : {"Histo_A1", "Histo_A2", "Histo_A3"}) + EXPECT_NE(dir->Get(name), nullptr) << name; + + EXPECT_EQ(outfile.Get("B"), nullptr); + EXPECT_EQ(outfile.Get("C"), nullptr); + + dir = outfile.Get("E"); + ASSERT_NE(dir, nullptr); + dir = dir->Get("F"); + ASSERT_NE(dir, nullptr); + for (auto name : {"Histo_F1", "Histo_F2", "Histo_F3"}) + EXPECT_NE(dir->Get(name), nullptr) << name; + + for (auto name : {input1, input2, output}) + gSystem->Unlink(name); +}