From: James Lai Date: Fri, 16 Jan 2026 14:21:31 +0000 (+0800) Subject: Add snbk visualize command to produce tree diagrams of snapshots (#1085) X-Git-Tag: v0.13.1~52 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4b56ae55a055a5084a9ecaf240681393fe2f19fe;p=thirdparty%2Fsnapper.git Add snbk visualize command to produce tree diagrams of snapshots (#1085) * Add snbk visualize command * Add bash completion for snbk visualize * Add documentation for snbk visualize command * Remove access by reference for Rankdir enumeration * Prevent graph text from being translated --- diff --git a/client/snbk/Makefile.am b/client/snbk/Makefile.am index d7095150..42e34c60 100644 --- a/client/snbk/Makefile.am +++ b/client/snbk/Makefile.am @@ -15,6 +15,7 @@ snbk_SOURCES = \ cmd-restore.cc \ cmd-delete.cc \ cmd-transfer-and-delete.cc \ + cmd-visualize.cc \ BackupConfig.cc BackupConfig.h \ TheBigThing.cc TheBigThing.h \ GlobalOptions.cc GlobalOptions.h \ diff --git a/client/snbk/TreeView.cc b/client/snbk/TreeView.cc index 1c27e591..54af02f5 100644 --- a/client/snbk/TreeView.cc +++ b/client/snbk/TreeView.cc @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024-2025] SUSE LLC + * Copyright (c) [2024-2026] SUSE LLC * * All Rights Reserved. * @@ -100,7 +100,7 @@ namespace snapper label = label + "\\n" + boost::join(properties, ", "); } - return sformat(_("%s [ label=\"%s\", style=\"%s\", shape=\"box\"]"), + return sformat("%s [ label=\"%s\", style=\"%s\", shape=\"box\"]", get_node_id(node).c_str(), label.c_str(), node->is_virtual() ? "dashed" : ""); } @@ -126,9 +126,8 @@ namespace snapper SN_THROW(Exception("Invalid parent type.")); } - cout << sformat(_(" %s -> %s [style=\"%s\"]"), - get_node_id(node).c_str(), get_node_id(child).c_str(), - link_style) + cout << sformat(" %s -> %s [style=\"%s\"]", get_node_id(node).c_str(), + get_node_id(child).c_str(), link_style) << '\n'; print_graph_graphviz_recursive(child); @@ -293,15 +292,15 @@ namespace snapper node->parent_type = parent_type; } - void TreeView::print_graph_graphviz(const ProxyNode* node, const string& rankdir) + void TreeView::print_graph_graphviz(const ProxyNode* node, const Rankdir rankdir) { cout << "digraph {" << '\n'; - cout << sformat(_(" rankdir=\"%s\"\n"), rankdir.c_str()); + cout << sformat(" rankdir=\"%s\"\n", toString(rankdir).c_str()); print_graph_graphviz_recursive(node); cout << "}" << '\n'; } - void TreeView::print_graph_graphviz(const string& rankdir) const + void TreeView::print_graph_graphviz(const Rankdir rankdir) const { TreeView::print_graph_graphviz(virtual_root.get(), rankdir); } @@ -311,4 +310,11 @@ namespace snapper "none", "direct-parent", "implicit-parent" }); + const vector EnumInfo::names({ + "TB", + "LR", + "BT", + "RL", + }); + } diff --git a/client/snbk/TreeView.h b/client/snbk/TreeView.h index f402e3a9..f6b88b1c 100644 --- a/client/snbk/TreeView.h +++ b/client/snbk/TreeView.h @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024-2025] SUSE LLC + * Copyright (c) [2024-2026] SUSE LLC * * All Rights Reserved. * @@ -52,6 +52,15 @@ namespace snapper IMPLICIT_PARENT /** Manually assigned. */ }; + /** Direction to draw directed graphs (Graphviz rankdir attribute). */ + enum class Rankdir + { + TB, + LR, + BT, + RL + }; + /** Proxy class to the nodes (snapshots). */ class ProxyNode { @@ -104,7 +113,7 @@ namespace snapper find_nearest_valid_node(const string& start_uuid) const; /** Print the tree graph in Graphviz DOT Language. */ - void print_graph_graphviz(const string& rankdir = "LR") const; + void print_graph_graphviz(const Rankdir rankdir = Rankdir::LR) const; private: @@ -138,7 +147,7 @@ namespace snapper * Print the tree graph in Graphviz DOT Language, starting from the given node. */ static void print_graph_graphviz(const ProxyNode* node, - const string& rankdir = "LR"); + const Rankdir rankdir = Rankdir::LR); /** * A static function that sets the parent relationship for the given two nodes. @@ -159,6 +168,11 @@ namespace snapper static const vector names; }; + template <> struct EnumInfo + { + static const vector names; + }; + } // namespace snapper #endif diff --git a/client/snbk/cmd-visualize.cc b/client/snbk/cmd-visualize.cc new file mode 100644 index 00000000..b3e6af66 --- /dev/null +++ b/client/snbk/cmd-visualize.cc @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026 SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you may + * find current contact information at www.novell.com. + */ + + +#include + +#include "../misc.h" +#include "../utils/help.h" + +#include "BackupConfig.h" +#include "GlobalOptions.h" +#include "TheBigThing.h" + + +namespace snapper +{ + using namespace std; + + namespace + { + + enum class Mode + { + SOURCE_TREE, + TARGET_TREE + }; + + } // namespace + + + template <> struct EnumInfo + { + static const vector names; + }; + + const vector EnumInfo::names({ + "source-tree", + "target-tree", + }); + + + void help_visualize() + { + cout << " " << _("Produce a specific graph in Graphviz DOT format:") << '\n' + << "\t" << _("snbk visualize ") << '\n' + << "\n" + << "\t" << _("Supported modes:") << '\n' + << "\t" + << _("- source-tree: Produce a tree diagram of the snapshots on the source.") + << '\n' + << "\t" + << _("- target-tree: Produce a tree diagram of the snapshots on the target.") + << '\n' + << '\n' + << _(" Options for the 'visualize' command:") << '\n'; + + print_options({ + { _("--rankdir, -r"), + _("The 'rankdir' diagram attribute of Graphviz. Defaults to 'LR'.") }, + }); + } + + void command_visualize(const GlobalOptions& global_options, GetOpts& get_opts, + BackupConfigs& backup_configs, ProxySnappers* snappers) + { + // Drawing a graph for multiple backup configs is not supported. + if (backup_configs.size() != 1) + { + SN_THROW(OptionsException(_("A backup-config must be specified to run this " + "command."))); + } + + BackupConfig& backup_config = backup_configs.front(); + + // Check and parse arguments + const vector