diff --git a/.clang-format b/.clang-format index a587cbb..0b98b25 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,6 @@ # Defines the Chromium style for automatic reformatting. # http://clang.llvm.org/docs/ClangFormatStyleOptions.html BasedOnStyle: Chromium -Standard: Cpp20 +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 diff --git a/scripts/check_clang_format.sh b/scripts/check_clang_format.sh old mode 100644 new mode 100755 diff --git a/scripts/run_clang_format.sh b/scripts/run_clang_format.sh old mode 100644 new mode 100755 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6dc4b39..ca57dcb 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -9,9 +9,7 @@ add_library(ostree-tui_core commit.cpp manager.cpp manager.hpp OSTreeTUI.cpp - OSTreeTUI.hpp - scroller.cpp - scroller.hpp) + OSTreeTUI.hpp) target_link_libraries(ostree-tui_core PRIVATE clip diff --git a/src/core/OSTreeTUI.cpp b/src/core/OSTreeTUI.cpp index 251440e..3ff9dee 100644 --- a/src/core/OSTreeTUI.cpp +++ b/src/core/OSTreeTUI.cpp @@ -1,292 +1,393 @@ #include "OSTreeTUI.hpp" +#include #include #include #include #include +#include #include -#include #include +#include -#include - -#include "commit.hpp" +#include // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp #include "ftxui/component/component.hpp" // for Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop #include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive -#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border - -#include "scroller.hpp" - -#include "footer.hpp" -#include "manager.hpp" +#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border #include "clip.h" #include "../util/cpplibostree.hpp" -std::vector OSTreeTUI::parseVisibleCommitMap(cpplibostree::OSTreeRepo& repo, - std::unordered_map& visibleBranches) { - - std::vector visibleCommitViewMap{}; - // get filtered commits - visibleCommitViewMap = {}; - for (const auto& commitPair : repo.getCommitList()) { - if (visibleBranches[commitPair.second.branch]) { - visibleCommitViewMap.push_back(commitPair.first); - } - } - // sort by date - std::sort(visibleCommitViewMap.begin(), visibleCommitViewMap.end(), [&](const std::string& a, const std::string& b) { - return repo.getCommitList().at(a).timestamp - > repo.getCommitList().at(b).timestamp; - }); - - return visibleCommitViewMap; -} +OSTreeTUI::OSTreeTUI(const std::string& repo, const std::vector& startupBranches) + : ostreeRepo(repo), selectedCommit(0), screen(ftxui::ScreenInteractive::Fullscreen()) { + using namespace ftxui; + + // set all branches as visible and define a branch color + for (const auto& branch : ostreeRepo.getBranches()) { + // if startupBranches are defined, set all as non-visible + visibleBranches[branch] = startupBranches.size() == 0 ? true : false; + std::hash nameHash{}; + branchColorMap[branch] = Color::Palette256((nameHash(branch) + 10) % 256); + } + // if startupBranches are defined, only set those visible + if (startupBranches.size() != 0) { + for (const auto& branch : startupBranches) { + visibleBranches[branch] = true; + } + } + + // COMMIT TREE + RefreshCommitComponents(); + + tree = Renderer([&] { + RefreshCommitComponents(); + selectedCommit = std::min(selectedCommit, visibleCommitViewMap.size() - 1); + // check for promotion & gray-out branch-colors if needed + if (inPromotionSelection && promotionBranch.size() != 0) { + std::unordered_map promotionBranchColorMap{}; + for (const auto& [str, col] : branchColorMap) { + if (str == promotionBranch) { + promotionBranchColorMap.insert({str, col}); + } else { + promotionBranchColorMap.insert({str, Color::GrayDark}); + } + } + return CommitRender::commitRender(*this, promotionBranchColorMap); + } + return CommitRender::commitRender(*this, branchColorMap); + }); + + commitListComponent = Container::Horizontal({tree, commitList}); + + // window specific shortcuts + commitListComponent = CatchEvent(commitListComponent, [&](Event event) { + // switch through commits + if ((!inPromotionSelection && event == Event::ArrowUp) || + (event.is_mouse() && event.mouse().button == Mouse::WheelUp)) { + selectedCommit = std::max(0, static_cast(selectedCommit) - 1); + adjustScrollToSelectedCommit(); + return true; + } + if ((!inPromotionSelection && event == Event::ArrowDown) || + (event.is_mouse() && event.mouse().button == Mouse::WheelDown)) { + selectedCommit = std::min(selectedCommit + 1, GetVisibleCommitViewMap().size() - 1); + adjustScrollToSelectedCommit(); + return true; + } + return false; + }); + + // INTERCHANGEABLE VIEW + // info + infoView = Renderer([&] { + parseVisibleCommitMap(); + if (visibleCommitViewMap.size() <= 0) { + return text(" no commit info available ") | color(Color::RedLight) | bold | center; + } + return CommitInfoManager::renderInfoView( + ostreeRepo.getCommitList().at(visibleCommitViewMap.at(selectedCommit))); + }); -int OSTreeTUI::main(const std::string& repo, const std::vector& startupBranches, bool showTooltips) { - using namespace ftxui; - - // - STATES ---------- ---------- - - // Model - cpplibostree::OSTreeRepo ostreeRepo(repo); - // View - size_t selectedCommit{0}; // view-index - std::unordered_map visibleBranches{}; // map branch visibility to branch - std::vector visibleCommitViewMap{}; // map from view-index to commit-hash - std::unordered_map branchColorMap{}; // map branch to color - std::string notificationText = ""; // footer notification - - // set all branches as visible and define a branch color - for (const auto& branch : ostreeRepo.getBranches()) { - // if startupBranches are defined, set all as non-visible - visibleBranches[branch] = startupBranches.size() == 0 ? true : false; - std::hash nameHash{}; - branchColorMap[branch] = Color::Palette256((nameHash(branch) + 10) % 256); - } - // if startupBranches are defined, only set those visible - if (startupBranches.size() != 0) { - for (const auto& branch : startupBranches) { - if (visibleBranches.find(branch) == visibleBranches.end()) { - return showHelp("ostree-tui","no such branch in repository " + repo + ": " + branch); - } - visibleBranches[branch] = true; - } - } - - // - UPDATES ---------- ---------- - - auto refresh_repository = [&] { - ostreeRepo.updateData(); - visibleCommitViewMap = parseVisibleCommitMap(ostreeRepo, visibleBranches); - return true; - }; - auto next_commit = [&] { - if (selectedCommit + 1 >= visibleCommitViewMap.size()) { - selectedCommit = visibleCommitViewMap.size() - 1; - return false; - } - ++selectedCommit; - return true; - }; - auto prev_commit = [&] { - if (selectedCommit <= 0) { - selectedCommit = 0; - return false; - } - --selectedCommit; - return true; - }; - - // - UI ELEMENTS ---------- ---------- - auto screen = ScreenInteractive::Fullscreen(); - - std::vector allBranches = ostreeRepo.getBranches(); - - // INTERCHANGEABLE VIEW - // info - Component infoView = Renderer([&] { - if (visibleCommitViewMap.size() <= 0) { - return text(" no commit info available ") | color(Color::RedLight) | bold | center; - } - return CommitInfoManager::renderInfoView(ostreeRepo.getCommitList().at(visibleCommitViewMap.at(selectedCommit))); + // filter + filterManager = + std::unique_ptr(new BranchBoxManager(*this, ostreeRepo, visibleBranches)); + filterView = + Renderer(filterManager->branchBoxes, [&] { return filterManager->branchBoxRender(); }); + + // interchangeable view (composed) + manager = std::unique_ptr(new Manager(*this, infoView, filterView)); + managerRenderer = manager->getManagerRenderer(); + + // FOOTER + FooterRenderer = Renderer([&] { return footer.FooterRender(); }); + + // BUILD MAIN CONTAINER + container = Component(managerRenderer); + container = ResizableSplitLeft(commitListComponent, container, &logSize); + container = ResizableSplitBottom(FooterRenderer, container, &footerSize); + + commitListComponent->TakeFocus(); + + // add application shortcuts + mainContainer = CatchEvent(container | border, [&](const Event& event) { + if (event == Event::AltP) { + SetPromotionMode(true, visibleCommitViewMap.at(selectedCommit)); + } + // copy commit id + if (event == Event::AltC) { + std::string hash = visibleCommitViewMap.at(selectedCommit); + clip::set_text(hash); + notificationText = " Copied Hash " + hash + " "; + return true; + } + // refresh repository + if (event == Event::AltR) { + RefreshOSTreeRepository(); + notificationText = " Refreshed Repository Data "; + return true; + } + // exit + if (event == Event::AltQ) { + screen.ExitLoopClosure()(); + return true; + } + // make commit list focussable + if (event == Event::ArrowLeft && managerRenderer->Focused() && + manager->getTabIndex() == 0) { + commitListComponent->TakeFocus(); + return true; + } + return false; }); +} - // filter - BranchBoxManager filterManager(ostreeRepo, visibleBranches); - Component filterView = Renderer(filterManager.branchBoxes, [&] { - return filterManager.branchBoxRender(); - }); - - // promotion - ContentPromotionManager promotionManager(showTooltips); - promotionManager.setBranchRadiobox(Radiobox(&allBranches, &promotionManager.selectedBranch)); - promotionManager.setApplyButton(Button(" Apply ", [&] { - ostreeRepo.promoteCommit(visibleCommitViewMap.at(selectedCommit), - ostreeRepo.getBranches().at(static_cast(promotionManager.selectedBranch)), - {}, promotionManager.newSubject, - true); - refresh_repository(); - notificationText = " Applied content promotion. "; - }, ButtonOption::Simple())); - Component promotionView = Renderer(promotionManager.composePromotionComponent(), [&] { - if (visibleCommitViewMap.size() <= 0) { - return text(" please select a commit to continue commit-promotion... ") | color(Color::RedLight) | bold | center; - } - return promotionManager.renderPromotionView(ostreeRepo, screen.dimy(), - ostreeRepo.getCommitList().at(visibleCommitViewMap.at(selectedCommit))); +int OSTreeTUI::Run() { + using namespace ftxui; + // footer notification update loader + // Probably not the best solution, having an active wait and should maybe + // only be started, once a notification is set... + bool runSubThreads{true}; + std::thread footerNotificationUpdater([&] { + while (runSubThreads) { + using namespace std::chrono_literals; + // notification is set + if (notificationText != "") { + footer.SetContent(notificationText); + screen.Post(Event::Custom); + std::this_thread::sleep_for(2s); + // clear notification + notificationText = ""; + footer.ResetContent(); + screen.Post(Event::Custom); + } + std::this_thread::sleep_for(0.2s); + } }); - // interchangeable view (composed) - Manager manager(infoView, filterView, promotionView); - Component managerRenderer = manager.managerRenderer; - - // COMMIT TREE - Component logRenderer = Scroller(&selectedCommit, CommitRender::COMMIT_DETAIL_LEVEL, Renderer([&] { - visibleCommitViewMap = parseVisibleCommitMap(ostreeRepo, visibleBranches); - selectedCommit = std::min(selectedCommit, visibleCommitViewMap.size() - 1); - return CommitRender::commitRender(ostreeRepo, visibleCommitViewMap, visibleBranches, branchColorMap, selectedCommit); - })); - - // FOOTER - Footer footer; - Component footerRenderer = Renderer([&] { - return footer.footerRender(); - }); - - // window specific shortcuts - logRenderer = CatchEvent(logRenderer, [&](Event event) { - // switch through commits - if (event == Event::ArrowUp || event == Event::Character('k') || (event.is_mouse() && event.mouse().button == Mouse::WheelUp)) { - return prev_commit(); - } - if (event == Event::ArrowDown || event == Event::Character('j') || (event.is_mouse() && event.mouse().button == Mouse::WheelDown)) { - return next_commit(); - } - return false; - }); - - int logSize{45}; - int footerSize{1}; - Component container{managerRenderer}; - container = ResizableSplitLeft(logRenderer, container, &logSize); - container = ResizableSplitBottom(footerRenderer, container, &footerSize); - - logRenderer->TakeFocus(); - - // add application shortcuts - Component mainContainer = CatchEvent(container | border, [&](const Event& event) { - // copy commit id - if (event == Event::AltC) { - std::string hash = visibleCommitViewMap.at(selectedCommit); - clip::set_text(hash); - notificationText = " Copied Hash " + hash + " "; - return true; - } - // refresh repository - if (event == Event::AltR) { - refresh_repository(); - notificationText = " Refreshed Repository Data "; - return true; - } - // exit - if (event == Event::AltQ || event == Event::Escape) { - screen.ExitLoopClosure()(); - return true; - } - return false; - }); - - // footer notification update loader - // Probably not the best solution, having an active wait and should maybe - // only be started, once a notification is set... - bool runSubThreads{true}; - std::thread footerNotificationUpdater([&] { - while (runSubThreads) { - using namespace std::chrono_literals; - // notification is set - if (notificationText != "") { - footer.content = notificationText; - screen.Post(Event::Custom); - std::this_thread::sleep_for(2s); - // clear notification - notificationText = ""; - footer.resetContent(); - screen.Post(Event::Custom); - } - std::this_thread::sleep_for(0.2s); - } - }); - - screen.Loop(mainContainer); - runSubThreads = false; - footerNotificationUpdater.join(); - - return EXIT_SUCCESS; + screen.Loop(mainContainer); + runSubThreads = false; + footerNotificationUpdater.join(); + + return EXIT_SUCCESS; } +void OSTreeTUI::RefreshCommitComponents() { + using namespace ftxui; + + commitComponents.clear(); + int i{0}; + parseVisibleCommitMap(); + for (auto& hash : visibleCommitViewMap) { + commitComponents.push_back(CommitRender::CommitComponent(i, hash, *this)); + i++; + } + + commitList = + commitComponents.size() == 0 + ? Renderer([&] { return text(" no commits to be shown ") | color(Color::Red); }) + : Container::Stacked(commitComponents); +} +void OSTreeTUI::RefreshCommitListComponent() { + using namespace ftxui; + + parseVisibleCommitMap(); + + commitListComponent->DetachAllChildren(); + RefreshCommitComponents(); + Component tmp = Container::Horizontal({tree, commitList}); + commitListComponent->Add(tmp); +} + +bool OSTreeTUI::RefreshOSTreeRepository() { + ostreeRepo.updateData(); + RefreshCommitListComponent(); + return true; +} + +bool OSTreeTUI::SetPromotionMode(bool active, const std::string& hash, bool SetPromotionBranch) { + // deactivate promotion mode + if (!active) { + inPromotionSelection = false; + promotionBranch = ""; + promotionHash = ""; + return true; + } + // set promotion mode + if (!inPromotionSelection || hash != promotionHash) { + inPromotionSelection = true; + if (SetPromotionBranch) { + promotionBranch = promotionBranch.empty() ? columnToBranchMap.at(0) : promotionBranch; + } + promotionHash = hash; + return true; + } + // nothing to update + return false; +} + +bool OSTreeTUI::PromoteCommit(const std::string& hash, + const std::string& targetBranch, + const std::vector& metadataStrings, + const std::string& newSubject, + bool keepMetadata) { + bool success = + ostreeRepo.PromoteCommit(hash, targetBranch, metadataStrings, newSubject, keepMetadata); + SetPromotionMode(false); + // reload repository + if (success) { + scrollOffset = 0; + selectedCommit = 0; + screen.PostEvent(ftxui::Event::AltR); + } + return success; +} + +void OSTreeTUI::parseVisibleCommitMap() { + // get filtered commits + visibleCommitViewMap = {}; + for (const auto& commitPair : ostreeRepo.getCommitList()) { + if (visibleBranches[commitPair.second.branch]) { + visibleCommitViewMap.push_back(commitPair.first); + } + } + // sort by date + std::sort(visibleCommitViewMap.begin(), visibleCommitViewMap.end(), + [&](const std::string& a, const std::string& b) { + return ostreeRepo.getCommitList().at(a).timestamp > + ostreeRepo.getCommitList().at(b).timestamp; + }); +} + +void OSTreeTUI::adjustScrollToSelectedCommit() { + // try to scroll it to the middle + int windowHeight = screen.dimy() - 4; + int scollOffsetToFitCommitToTop = -selectedCommit * CommitRender::COMMIT_WINDOW_HEIGHT; + int newScroll = + scollOffsetToFitCommitToTop + windowHeight / 2 - CommitRender::COMMIT_WINDOW_HEIGHT; + // adjust if on edges + int min = 0; + int max = -windowHeight - static_cast(visibleCommitViewMap.size() - 1) * + CommitRender::COMMIT_WINDOW_HEIGHT; + scrollOffset = std::max(newScroll, max); + scrollOffset = std::min(min, newScroll); +} + +// SETTER & non-const GETTER +void OSTreeTUI::SetPromotionBranch(const std::string& promotionBranch) { + this->promotionBranch = promotionBranch; +} + +void OSTreeTUI::SetSelectedCommit(size_t selectedCommit) { + this->selectedCommit = selectedCommit; + adjustScrollToSelectedCommit(); +} + +std::vector& OSTreeTUI::GetColumnToBranchMap() { + return columnToBranchMap; +} + +ftxui::ScreenInteractive& OSTreeTUI::GetScreen() { + return screen; +} + +// GETTER +const cpplibostree::OSTreeRepo& OSTreeTUI::GetOstreeRepo() const { + return ostreeRepo; +} + +const size_t& OSTreeTUI::GetSelectedCommit() const { + return selectedCommit; +} + +const std::string& OSTreeTUI::GetPromotionBranch() const { + return promotionBranch; +} + +const std::unordered_map& OSTreeTUI::GetVisibleBranches() const { + return visibleBranches; +} + +const std::vector& OSTreeTUI::GetColumnToBranchMap() const { + return columnToBranchMap; +} + +const std::vector& OSTreeTUI::GetVisibleCommitViewMap() const { + return visibleCommitViewMap; +} + +const std::unordered_map& OSTreeTUI::GetBranchColorMap() const { + return branchColorMap; +} + +int OSTreeTUI::GetScrollOffset() const { + return scrollOffset; +} + +bool OSTreeTUI::GetInPromotionSelection() const { + return inPromotionSelection; +} + +const std::string& OSTreeTUI::GetPromotionHash() const { + return promotionHash; +} + +// STATIC int OSTreeTUI::showHelp(const std::string& caller, const std::string& errorMessage) { - using namespace ftxui; - - // define command line options - std::vector> command_options{ - // option, arguments, meaning - {"-h, --help", "", "Show help options. The REPOSITORY_PATH can be omitted"}, - {"-r, --refs", "REF [REF...]", "Specify a list of visible refs at startup if not specified, show all refs"}, - {"-n, --no-tooltips", "", "Hide Tooltips in promotion view."} - }; - - Elements options {text("Options:")}; - Elements arguments {text("Arguments:")}; - Elements meanings {text("Meaning:")}; - for (const auto& command : command_options) { - options.push_back(text(command.at(0) + " ") | color(Color::GrayLight)); - arguments.push_back(text(command.at(1) + " ") | color(Color::GrayLight)); - meanings.push_back(text(command.at(2) + " ")); - } - - auto helpPage = vbox({ - errorMessage.empty() ? filler() : (text(errorMessage) | bold | color(Color::Red) | flex), - hbox({ - text("Usage: "), - text(caller) | color(Color::GrayLight), - text(" REPOSITORY_PATH") | color(Color::Yellow), - text(" [OPTION...]") | color(Color::Yellow) - }), - text(""), - hbox({ - vbox(options), - vbox(arguments), - vbox(meanings), - }), - text(""), - hbox({ - text("Report bugs at "), - text("Github.com/AP-Sensing/ostree-tui") | hyperlink("https://github.com/AP-Sensing/ostree-tui") - }), - text("") - }); - - auto screen = Screen::Create(Dimension::Fit(helpPage)); - Render(screen, helpPage); - screen.Print(); - std::cout << "\n"; - - return errorMessage.empty(); + using namespace ftxui; + + // define command line options + std::vector> command_options{ + // option, arguments, meaning + {"-h, --help", "", "Show help options. The REPOSITORY_PATH can be omitted"}, + {"-r, --refs", "REF [REF...]", + "Specify a list of visible refs at startup if not specified, show all refs"}, + }; + + Elements options{text("Options:")}; + Elements arguments{text("Arguments:")}; + Elements meanings{text("Meaning:")}; + for (const auto& command : command_options) { + options.push_back(text(command.at(0) + " ") | color(Color::GrayLight)); + arguments.push_back(text(command.at(1) + " ") | color(Color::GrayLight)); + meanings.push_back(text(command.at(2) + " ")); + } + + auto helpPage = vbox( + {errorMessage.empty() ? filler() : (text(errorMessage) | bold | color(Color::Red) | flex), + hbox({text("Usage: "), text(caller) | color(Color::GrayLight), + text(" REPOSITORY_PATH") | color(Color::Yellow), + text(" [OPTION...]") | color(Color::Yellow)}), + text(""), + hbox({ + vbox(options), + vbox(arguments), + vbox(meanings), + }), + text(""), + hbox({text("Report bugs at "), text("Github.com/AP-Sensing/ostree-tui") | + hyperlink("https://github.com/AP-Sensing/ostree-tui")}), + text("")}); + + auto screen = Screen::Create(Dimension::Fit(helpPage)); + Render(screen, helpPage); + screen.Print(); + std::cout << "\n"; + + return errorMessage.empty(); } int OSTreeTUI::showVersion() { - using namespace ftxui; - - auto versionText = text("ostree-tui 0.2.1"); - - auto screen = Screen::Create(Dimension::Fit(versionText)); - Render(screen, versionText); - screen.Print(); - std::cout << "\n"; - - return 0; + using namespace ftxui; + + auto versionText = text("ostree-tui 0.3.0"); + + auto screen = Screen::Create(Dimension::Fit(versionText)); + Render(screen, versionText); + screen.Print(); + std::cout << "\n"; + + return 0; } diff --git a/src/core/OSTreeTUI.hpp b/src/core/OSTreeTUI.hpp index 9ae549b..14ce501 100644 --- a/src/core/OSTreeTUI.hpp +++ b/src/core/OSTreeTUI.hpp @@ -3,38 +3,161 @@ | A terminal user interface for OSTree. |___________________________________________________________*/ +#pragma once + +#include #include #include +#include "ftxui/component/component.hpp" // for Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop +#include "ftxui/component/component_base.hpp" // for ComponentBase +#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive +#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border + +#include "commit.hpp" +#include "footer.hpp" +#include "manager.hpp" + #include "../util/cpplibostree.hpp" -namespace OSTreeTUI { +class OSTreeTUI { + public: + /** + * @brief Constructs, builds and assembles all components of the OSTreeTUI. + * + * @param repo Path to the OSTree repository directory. + * @param startupBranches Optional list of branches to pre-select at startup (providing nothing + * will display all branches). + */ + explicit OSTreeTUI(const std::string& repo, + const std::vector& startupBranches = {}); /** - * @brief OSTree TUI main - * - * @param repo ostree repository path + * @brief Runs the OSTreeTUI (starts the ftxui screen loop). + * + * @return Exit Code */ - int main(const std::string& repo, const std::vector& startupBranches = {}, bool showTooltips = true); + int Run(); + + /// @brief OSTreeTUI Refresh Level 3: Refreshes the commit components. + void RefreshCommitComponents(); + + /// @brief OSTreeTUI Refresh Level 2: Refreshes the commit list component & upper levels. + void RefreshCommitListComponent(); + + /// @brief OSTreeTUI Refresh Level 1: Refreshes complete repository & upper levels. + bool RefreshOSTreeRepository(); /** - * @brief Print help page - * - * @param caller argv[0] - * @param errorMessage optional error message to print on top - * @return 0, if no error message provided - * @return 1, if error message is provided, assuming bad program stop + * @brief Sets the promotion mode: Defines if the ostree-tui currently displays a commit + * promotion window. + * + * @param active Activate (true), or deactivate (false) the promotion mode. + * @param hash Must only be provided, if `active` is set to true. + * @param SetPromotionBranch If `active` is true, this defines, if the promotino Branch should + * be reset to the first visible branch. + * @return false, if other promotion gets overwritten */ - int showHelp(const std::string& caller, const std::string& errorMessage = ""); + bool SetPromotionMode(bool active, + const std::string& hash = "", + bool SetPromotionBranch = true); /** - * @brief Print the application version - * - * @return int + * @brief Promotes a commit, by passing it to the cpplibostree and refreshing the UI. + * + * @param hash Hash of commit to be promoted. + * @param targetBranch Branch to promote the commit to. + * @param metadataStrings Optional additional metadata-strings to be set. + * @param newSubject New commit subject. + * @param keepMetadata Keep metadata of old commit. + * @return promotion success */ - int showVersion(); + bool PromoteCommit(const std::string& hash, + const std::string& targetBranch, + const std::vector& metadataStrings = {}, + const std::string& newSubject = "", + bool keepMetadata = true); + + private: + /// @brief Calculates all visible commits from an OSTreeRepo and a list of branches. + void parseVisibleCommitMap(); + + /// @brief Adjust scroll offset to fit the selected commit. + void adjustScrollToSelectedCommit(); + + public: + // SETTER + void SetPromotionBranch(const std::string& promotionBranch); + void SetSelectedCommit(size_t selectedCommit); - std::vector parseVisibleCommitMap(cpplibostree::OSTreeRepo& repo, - std::unordered_map& visibleBranches); + // non-const GETTER + [[nodiscard]] std::vector& GetColumnToBranchMap(); + [[nodiscard]] ftxui::ScreenInteractive& GetScreen(); -} // namespace OSTreeTUI + // GETTER + [[nodiscard]] const cpplibostree::OSTreeRepo& GetOstreeRepo() const; + [[nodiscard]] const size_t& GetSelectedCommit() const; + [[nodiscard]] const std::string& GetPromotionBranch() const; + [[nodiscard]] const std::unordered_map& GetVisibleBranches() const; + [[nodiscard]] const std::vector& GetColumnToBranchMap() const; + [[nodiscard]] const std::vector& GetVisibleCommitViewMap() const; + [[nodiscard]] const std::unordered_map& GetBranchColorMap() const; + [[nodiscard]] int GetScrollOffset() const; + [[nodiscard]] bool GetInPromotionSelection() const; + [[nodiscard]] const std::string& GetPromotionHash() const; + + private: + // model + cpplibostree::OSTreeRepo ostreeRepo; + + // backend states + size_t selectedCommit; + std::unordered_map visibleBranches; // map branch -> visibe + std::vector columnToBranchMap; // map branch -> column in commit-tree + std::vector visibleCommitViewMap; // map view-index -> commit-hash + std::unordered_map branchColorMap; // map branch -> color + std::string notificationText; // footer notification + + // view states + int scrollOffset{0}; + bool inPromotionSelection{false}; + std::string promotionHash; + std::string promotionBranch; + + // view constants + int logSize{45}; + int footerSize{1}; + + // components + Footer footer; + std::unique_ptr filterManager{nullptr}; + std::unique_ptr manager{nullptr}; + ftxui::ScreenInteractive screen; + ftxui::Component mainContainer; + ftxui::Components commitComponents; + ftxui::Component commitList; + ftxui::Component tree; + ftxui::Component commitListComponent; + ftxui::Component infoView; + ftxui::Component filterView; + ftxui::Component managerRenderer; + ftxui::Component FooterRenderer; + ftxui::Component container; + + public: + /** + * @brief Print a help page including usage, options, etc. + * + * @param caller argv[0] + * @param errorMessage Optional error message to print on top. + * @return Exit Code + */ + static int showHelp(const std::string& caller, const std::string& errorMessage = ""); + + /** + * @brief Print the application version + * + * @return Exit Code + */ + static int showVersion(); +}; diff --git a/src/core/commit.cpp b/src/core/commit.cpp index f7e0fa0..e514ada 100644 --- a/src/core/commit.cpp +++ b/src/core/commit.cpp @@ -1,163 +1,454 @@ #include "commit.hpp" +#include +#include #include +#include #include #include +#include #include -#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border +#include // for Component, ComponentBase +#include +#include // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp +#include // for ScreenInteractive +#include "ftxui/component/component.hpp" // for Make +#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::WheelDown, Mouse::WheelUp +#include "ftxui/dom/elements.hpp" // for operator|, Element, size, vbox, EQUAL, HEIGHT, dbox, reflect, focus, inverted, nothing, select, vscroll_indicator, yflex, yframe +#include "ftxui/dom/node.hpp" // for Node +#include "ftxui/screen/box.hpp" // for Box +#include "ftxui/screen/color.hpp" // for Color #include "../util/cpplibostree.hpp" +#include "OSTreeTUI.hpp" + namespace CommitRender { -void addLine(const RenderTree& treeLineType, const RenderLine& lineType, - ftxui::Elements& treeElements, ftxui::Elements& commElements, - const cpplibostree::Commit& commit, - const bool& highlight, - const std::unordered_map& usedBranches, - const std::unordered_map& branchColorMap) { - treeElements.push_back(addTreeLine(treeLineType, commit, usedBranches, branchColorMap)); - commElements.push_back(addCommLine(lineType, commit, highlight, branchColorMap)); +namespace { +using namespace ftxui; + +/// From https://github.com/ArthurSonzogni/FTXUI/blob/main/src/ftxui/component/window.cpp +Decorator PositionAndSize(int left, int top, int width, int height) { + return [=](Element element) { + element |= size(WIDTH, EQUAL, width); + element |= size(HEIGHT, EQUAL, height); + + auto padding_left = emptyElement() | size(WIDTH, EQUAL, left); + auto padding_top = emptyElement() | size(HEIGHT, EQUAL, top); + + return vbox({ + padding_top, + hbox({ + padding_left, + element, + }), + }); + }; } -ftxui::Element commitRender(cpplibostree::OSTreeRepo& repo, - const std::vector& visibleCommitMap, - const std::unordered_map& visibleBranches, - const std::unordered_map& branchColorMap, - size_t selectedCommit) { - using namespace ftxui; - - // check empty commit list - if (visibleCommitMap.size() <= 0 || visibleBranches.size() <= 0) { - return color(Color::RedLight, text(" no commits to be shown ") | bold | center); - } - - // stores the dedicated tree-column of each branch, -1 meaning not displayed yet - std::unordered_map usedBranches{}; - for (const auto& branchPair : visibleBranches) { - if (branchPair.second) { - usedBranches[branchPair.first] = -1; - } - } - int nextAvailableSpace = usedBranches.size() - 1; - - // - RENDER - - // left tree, right commits - Elements treeElements{}; - Elements commElements{}; - - std::string markedString = repo.getCommitList().at(visibleCommitMap.at(selectedCommit)).hash; - for (const auto& visibleCommitIndex : visibleCommitMap) { - cpplibostree::Commit commit = repo.getCommitList().at(visibleCommitIndex); - bool highlight = markedString == commit.hash; - // branch head if it is first branch usage - std::string relevantBranch = commit.branch; - if (usedBranches.at(relevantBranch) == -1) { - usedBranches.at(relevantBranch) = nextAvailableSpace--; - addLine(RenderTree::TREE_LINE_IGNORE_BRANCH, RenderLine::BRANCH_HEAD, - treeElements, commElements, commit, highlight, usedBranches, branchColorMap); - } - // commit - addLine(RenderTree::TREE_LINE_NODE, RenderLine::COMMIT_HASH, - treeElements, commElements, commit, highlight, usedBranches, branchColorMap); - addLine(RenderTree::TREE_LINE_TREE, RenderLine::COMMIT_DATE, - treeElements, commElements, commit, highlight, usedBranches, branchColorMap); - addLine(RenderTree::TREE_LINE_TREE, RenderLine::EMPTY, - treeElements, commElements, commit, highlight, usedBranches, branchColorMap); - } - - return hbox({ - vbox(std::move(treeElements)), - vbox(std::move(commElements)) - }); +/// Partially inspired from +/// https://github.com/ArthurSonzogni/FTXUI/blob/main/src/ftxui/component/window.cpp +Element DefaultRenderState(const WindowRenderState& state, + ftxui::Color selectedColor = Color::White, + bool dimmable = true) { + Element element = state.inner; + if (!dimmable) { + selectedColor = Color::White; + } + if (selectedColor == Color::White && dimmable) { + element |= dim; + } else { + element |= bold; + } + element |= color(selectedColor); + + element = window(text(state.title), element); + element |= clear_under; + + return element; } -ftxui::Element addTreeLine(const RenderTree& treeLineType, - const cpplibostree::Commit& commit, - const std::unordered_map& usedBranches, - const std::unordered_map& branchColorMap) { - using namespace ftxui; - - std::string relevantBranch = commit.branch; - // create an empty branch tree line - Elements tree(usedBranches.size(), text(COMMIT_NONE)); - - // populate tree with all displayed branches - for (const auto& branch : usedBranches) { - if (branch.second == -1) { - continue; - } - - if (treeLineType == RenderTree::TREE_LINE_IGNORE_BRANCH && branch.first != relevantBranch) { - tree.at(branch.second) = (text(COMMIT_TREE) | color(branchColorMap.at(branch.first))); - } else if (treeLineType == RenderTree::TREE_LINE_NODE) { - if (branch.first == relevantBranch) { - tree.at(branch.second) = (text(COMMIT_NODE) | color(branchColorMap.at(branch.first))); - } else { - tree.at(branch.second) = (text(COMMIT_TREE) | color(branchColorMap.at(branch.first))); - } - } else if (treeLineType == RenderTree::TREE_LINE_TREE) { - tree.at(branch.second) = (text(COMMIT_TREE) | color(branchColorMap.at(branch.first))); - } +/// Draggable commit window, including ostree-tui logic for overlap detection, etc. +/// Partially inspired from +/// https://github.com/ArthurSonzogni/FTXUI/blob/main/src/ftxui/component/window.cpp +class CommitComponentImpl : public ComponentBase, public WindowOptions { + public: + explicit CommitComponentImpl(int position, std::string commit, OSTreeTUI& ostreetui) + : commitPosition(position), + hash(std::move(commit)), + ostreetui(ostreetui), + commit(ostreetui.GetOstreeRepo().getCommitList().at(hash)), + newVersion(this->commit.version), + drag_initial_y(position * COMMIT_WINDOW_HEIGHT), + drag_initial_x(1) { + inner = Renderer([&] { + return vbox({ + text(ostreetui.GetOstreeRepo().getCommitList().at(hash).subject), + text( + std::format("{:%Y-%m-%d %T %Ez}", + std::chrono::time_point_cast( + ostreetui.GetOstreeRepo().getCommitList().at(hash).timestamp))), + }); + }); + simpleCommit = inner; + Add(inner); + + title = hash.substr(0, 8); + top = drag_initial_y; + left = drag_initial_x; + width = COMMIT_WINDOW_WIDTH; + height = COMMIT_WINDOW_HEIGHT; } - return hbox(std::move(tree)); + private: + void resetWindow(bool positionReset = true) { + if (positionReset) { + left() = drag_initial_x; + top() = drag_initial_y; + } + width() = width_initial; + height() = height_initial; + // reset window contents + DetachAllChildren(); + Add(simpleCommit); + } + + void startPromotionWindow() { + left() = std::max(left(), -2); + width() = PROMOTION_WINDOW_WIDTH; + height() = PROMOTION_WINDOW_HEIGHT; + // change inner to promotion layout + simpleCommit = inner; + DetachAllChildren(); + Add(promotionView); + if (!Focused()) { + TakeFocus(); + // manually fix focus issues + // change to proper control layout if possible... + ostreetui.GetScreen().Post(Event::ArrowDown); + ostreetui.GetScreen().Post(Event::ArrowRight); + ostreetui.GetScreen().Post(Event::ArrowDown); + ostreetui.GetScreen().Post(Event::ArrowRight); + ostreetui.GetScreen().Post(Event::ArrowUp); + } + } + + void executePromotion() { + // promote on the ostree repo + std::vector metadataStrings; + if (!newVersion.empty()) { + metadataStrings.push_back("version=" + newVersion); + } + ostreetui.PromoteCommit(hash, ostreetui.GetPromotionBranch(), metadataStrings, newSubject, + true); + resetWindow(); + } + + void cancelPromotion() { + ostreetui.SetPromotionMode(false); + resetWindow(); + } + + Element Render() final { + // check if promotion was started not from drag & drop, but from ostreetui + if (ostreetui.GetInPromotionSelection() && ostreetui.GetPromotionHash() == hash) { + if (!ostreetui.GetPromotionBranch().empty()) { + startPromotionWindow(); + } else { + resetWindow(false); + } + } + + auto element = ComponentBase::Render(); + + const WindowRenderState state = {element, title(), Active(), drag_}; + + if (commitPosition == ostreetui.GetSelectedCommit()) { // selected & not in promotion + element = + render ? render(state) + : DefaultRenderState(state, ostreetui.GetBranchColorMap().at(commit.branch), + ostreetui.GetPromotionHash() != hash); + } else { + element = render ? render(state) + : DefaultRenderState(state, Color::White, + ostreetui.GetPromotionHash() != hash); + } + + // Position and record the drawn area of the window. + element |= reflect(box_window_); + element |= PositionAndSize(left(), top() + ostreetui.GetScrollOffset(), width(), height()); + element |= reflect(box_); + + return element; + } + + bool OnEvent(Event event) final { + if (ComponentBase::OnEvent(event)) { + return true; + } + + if (ostreetui.GetInPromotionSelection()) { + // navigate promotion branches + if (event == Event::ArrowLeft) { + const long int it = std::find(ostreetui.GetColumnToBranchMap().begin(), + ostreetui.GetColumnToBranchMap().end(), + ostreetui.GetPromotionBranch()) - + ostreetui.GetColumnToBranchMap().begin(); + ostreetui.SetPromotionBranch(ostreetui.GetColumnToBranchMap().at( + (it - 1) % ostreetui.GetColumnToBranchMap().size())); + return true; + } + if (event == Event::ArrowRight) { + const long int it = std::find(ostreetui.GetColumnToBranchMap().begin(), + ostreetui.GetColumnToBranchMap().end(), + ostreetui.GetPromotionBranch()) - + ostreetui.GetColumnToBranchMap().begin(); + ostreetui.SetPromotionBranch(ostreetui.GetColumnToBranchMap().at( + (it + 1) % ostreetui.GetColumnToBranchMap().size())); + return true; + } + // cancel + if (event == Event::Escape) { + cancelPromotion(); + return true; + } + } + + if (!event.is_mouse()) { + return false; + } + + if (ostreetui.GetInPromotionSelection() && !drag_) { + return true; + } + + mouse_hover_ = box_window_.Contain(event.mouse().x, event.mouse().y); + + if (mouse_hover_ && event.mouse().button == Mouse::Left) { + ostreetui.SetSelectedCommit(commitPosition); + } + + if (captured_mouse_) { + if (event.mouse().motion == Mouse::Released) { + // reset mouse + captured_mouse_ = nullptr; + // check if position matches branch & do something if it does + if (ostreetui.GetPromotionBranch().empty()) { + ostreetui.SetPromotionMode(false, hash); + resetWindow(); + } else { + ostreetui.SetPromotionMode(true, hash); + } + return true; + } + + if (drag_) { + left() = event.mouse().x - drag_start_x - box_.x_min; + top() = event.mouse().y - drag_start_y - box_.y_min; + // potential promotion + ostreetui.SetPromotionMode(true, hash, false); + // calculate which branch currently is hovered over + ostreetui.SetPromotionBranch(""); + const int branch_pos = event.mouse().x / 2; + int count{0}; + for (const auto& [branch, visible] : ostreetui.GetVisibleBranches()) { + if (visible) { + ++count; + } + if (count == branch_pos) { + ostreetui.SetPromotionBranch( + ostreetui.GetColumnToBranchMap().at(event.mouse().x / 2 - 1)); + break; + } + } + } else { + // not promotion + ostreetui.SetPromotionMode(false); + } + + // Clamp the window size. + width() = std::max(width(), static_cast(title().size() + 2)); + height() = std::max(height(), 2); + + return true; + } + + if (!mouse_hover_) { + return false; + } + + if (!CaptureMouse(event)) { + return true; + } + + if (event.mouse().button != Mouse::Left) { + return true; + } + if (event.mouse().motion != Mouse::Pressed) { + return true; + } + + TakeFocus(); + + captured_mouse_ = CaptureMouse(event); + if (!captured_mouse_) { + return true; + } + + drag_start_x = event.mouse().x - left() - box_.x_min; + drag_start_y = event.mouse().y - top() - box_.y_min; + + const bool drag_old = drag_; + drag_ = true; + if (!drag_old && drag_) { // if we start dragging + drag_initial_x = left(); + drag_initial_y = top(); + } + return true; + } + + // window specific members + Box box_; + Box box_window_; + + CapturedMouse captured_mouse_; + int drag_start_x = 0; + int drag_start_y = 0; + + int drag_initial_x; + int drag_initial_y; + int width_initial = COMMIT_WINDOW_WIDTH; + int height_initial = COMMIT_WINDOW_HEIGHT; + + bool mouse_hover_ = false; + bool drag_ = false; + + // ostree-tui specific members + int commitPosition; + std::string hash; + OSTreeTUI& ostreetui; + + // promotion view + cpplibostree::Commit commit; + std::string newSubject; + std::string newVersion; + Component simpleCommit = Renderer([] { return text("error in commit window creation"); }); + Component promotionView = Container::Vertical( + {Renderer([&] { + return vbox({ + text(""), + text(" Promote Commit...") | bold, + text(""), + text(" ☐ " + hash.substr(0, 8)) | bold, + }); + }), + Container::Horizontal({Renderer([&] { return text(" ┆ subject: "); }), + Input(&newSubject, "enter new subject...") | underlined}), + // render version, if available + commit.version.empty() + ? Renderer([] { return filler(); }) + : Container::Horizontal({Renderer([&] { return text(" ┆ version: "); }), + Input(&newVersion, commit.version) | underlined}), + Renderer([&] { + return vbox({text(" ┆"), text(" ┆ to branch:"), + text(" ☐ " + ostreetui.GetPromotionBranch()) | bold, text(" │") | bold}); + }), + Container::Horizontal({ + Button(" Cancel ", [&] { cancelPromotion(); }) | color(Color::Red) | flex, + Button(" Promote ", [&] { executePromotion(); }) | color(Color::Green) | flex, + })}); +}; + +} // namespace + +ftxui::Component CommitComponent(int position, const std::string& commit, OSTreeTUI& ostreetui) { + return ftxui::Make(position, commit, ostreetui); +} + +ftxui::Element commitRender(OSTreeTUI& ostreetui, + const std::unordered_map& branchColorMap) { + using namespace ftxui; + + int scrollOffset = ostreetui.GetScrollOffset(); + + // check empty commit list + if (ostreetui.GetVisibleCommitViewMap().empty() || ostreetui.GetVisibleBranches().empty()) { + return color(Color::RedLight, text(" no commits to be shown ") | bold | center); + } + + // stores the dedicated tree-column of each branch, -1 meaning not displayed yet + std::unordered_map usedBranches{}; + for (const auto& branchPair : ostreetui.GetVisibleBranches()) { + if (branchPair.second) { + usedBranches[branchPair.first] = -1; + } + } + int nextAvailableSpace = static_cast(usedBranches.size() - 1); + + // - RENDER - + Elements treeElements{}; + + ostreetui.GetColumnToBranchMap().clear(); + for (const auto& visibleCommitIndex : ostreetui.GetVisibleCommitViewMap()) { + const cpplibostree::Commit commit = + ostreetui.GetOstreeRepo().getCommitList().at(visibleCommitIndex); + // branch head if it is first branch usage + const std::string relevantBranch = commit.branch; + if (usedBranches.at(relevantBranch) == -1) { + ostreetui.GetColumnToBranchMap().push_back(relevantBranch); + usedBranches.at(relevantBranch) = nextAvailableSpace--; + } + // commit + if (scrollOffset++ >= 0) { + treeElements.push_back( + addTreeLine(RenderTree::TREE_LINE_NODE, commit, usedBranches, branchColorMap)); + } + for (int i{0}; i < 3; i++) { + if (scrollOffset++ >= 0) { + treeElements.push_back( + addTreeLine(RenderTree::TREE_LINE_TREE, commit, usedBranches, branchColorMap)); + } + } + } + std::reverse(ostreetui.GetColumnToBranchMap().begin(), ostreetui.GetColumnToBranchMap().end()); + + return vbox(std::move(treeElements)); } +ftxui::Element addTreeLine(const RenderTree& treeLineType, + const cpplibostree::Commit& commit, + const std::unordered_map& usedBranches, + const std::unordered_map& branchColorMap) { + using namespace ftxui; + + const std::string relevantBranch = commit.branch; + // create an empty branch tree line + Elements tree(usedBranches.size(), text(COMMIT_NONE)); -ftxui::Element addCommLine(RenderLine lineType, - const cpplibostree::Commit& commit, - const bool& highlight, - const std::unordered_map& branchColorMap) { - using namespace ftxui; - - std::string relevantBranch = commit.branch; - Elements comm; - - switch (lineType) { - case EMPTY: { - comm.push_back(text("")); - break; - } - case BRANCH_HEAD: { - comm.push_back(text(relevantBranch) | color(branchColorMap.at(relevantBranch))); - break; - } - case COMMIT_HASH: { - // length adapted hash - std::string commitTopText = commit.hash; - if (commitTopText.size() > 8) { - commitTopText = GAP_TREE_COMMITS + commit.hash.substr(0, 8); - } - Element commitTopTextElement = text(commitTopText); - // highlighted / selected - if (highlight) { - commitTopTextElement = commitTopTextElement | bold | inverted; - } - // signed - if (cpplibostree::OSTreeRepo::isCommitSigned(commit)) { - std::string signedText = " signed " + (commit.signatures.size() > 1 ? std::to_string(commit.signatures.size()) + "x" : ""); - commitTopTextElement = hbox(commitTopTextElement, text(signedText) | color(Color::Green)); - } - comm.push_back(commitTopTextElement); - break; - } - case COMMIT_DATE: { - std::string ts = std::format("{:%Y-%m-%d %T %Ez}", - std::chrono::time_point_cast(commit.timestamp)); - comm.push_back(text(GAP_TREE_COMMITS + ts)); - break; - } - case COMMIT_SUBJ: { - std::string ts = std::format("{:%Y-%m-%d %T %Ez}", - std::chrono::time_point_cast(commit.timestamp)); - comm.push_back(paragraph(GAP_TREE_COMMITS + commit.subject)); - break; - } + // populate tree with all displayed branches + for (const auto& branch : usedBranches) { + if (branch.second == -1) { + continue; + } + + if (treeLineType == RenderTree::TREE_LINE_TREE || + (treeLineType == RenderTree::TREE_LINE_IGNORE_BRANCH && + branch.first != relevantBranch)) { + tree.at(branch.second) = (text(COMMIT_TREE) | color(branchColorMap.at(branch.first))); + } else if (treeLineType == RenderTree::TREE_LINE_NODE) { + if (branch.first == relevantBranch) { + tree.at(branch.second) = + (text(COMMIT_NODE) | color(branchColorMap.at(branch.first))); + } else { + tree.at(branch.second) = + (text(COMMIT_TREE) | color(branchColorMap.at(branch.first))); + } + } } - return hbox(std::move(comm)); + return hbox(std::move(tree)); } } // namespace CommitRender diff --git a/src/core/commit.hpp b/src/core/commit.hpp index f4aa2bc..3825900 100644 --- a/src/core/commit.hpp +++ b/src/core/commit.hpp @@ -11,98 +11,73 @@ #pragma once -#include #include #include -#include "../util/cpplibostree.hpp" - -namespace CommitRender { +#include +#include +#include "ftxui/component/component_base.hpp" - constexpr size_t COMMIT_DETAIL_LEVEL {3}; // lines per commit +#include "../util/cpplibostree.hpp" - constexpr std::string COMMIT_NODE {" ☐"}; - constexpr std::string COMMIT_TREE {" │"}; - constexpr std::string COMMIT_NONE {" "}; - constexpr std::string INDENT {" "}; - constexpr std::string GAP_TREE_COMMITS {" "}; +class OSTreeTUI; - enum RenderTree { - TREE_LINE_NODE, // ☐ | | - TREE_LINE_TREE, // | | | - TREE_LINE_IGNORE_BRANCH // | | - }; +namespace CommitRender { +// UI characters +constexpr std::string COMMIT_NODE{" ☐"}; +constexpr std::string COMMIT_TREE{" │"}; +constexpr std::string COMMIT_NONE{" "}; +// window dimensions +constexpr int COMMIT_WINDOW_HEIGHT{4}; +constexpr int COMMIT_WINDOW_WIDTH{32}; +constexpr int PROMOTION_WINDOW_HEIGHT{COMMIT_WINDOW_HEIGHT + 11}; +constexpr int PROMOTION_WINDOW_WIDTH{COMMIT_WINDOW_WIDTH + 8}; +// render tree types +enum RenderTree : uint8_t { + TREE_LINE_NODE, // ☐ | | + TREE_LINE_TREE, // | | | + TREE_LINE_IGNORE_BRANCH // | | +}; - enum RenderLine { - EMPTY, // - BRANCH_HEAD, // foo/bar - COMMIT_HASH, // a0f33cd9 - COMMIT_DATE, // 2024-04-27 09:12:55 +00:00 - COMMIT_SUBJ // Some Subject - }; +/** + * @brief Creates a window, containing a hash and some of its details. + * The window has a pre-defined position and snaps back to it, + * after being dragged & let go. When hovering over a branch, + * defined in `columnToBranchMap`, the window expands to a commit + * promotion window. + * To be used with other windows, use a `ftxui::Component::Stacked`. + * + * @return UI Component + */ +[[nodiscard]] ftxui::Component CommitComponent(int position, + const std::string& commit, + OSTreeTUI& ostreetui); - /** - * @brief create a Renderer for the commit section - * - * @param repo OSTree repository - * @param visibleCommitMap List of visible commit hashes - * @param visibleBranches List of visible branches - * @param branchColorMap Map from branch to its display color - * @param selectedCommit Commit that should be marked as selected - * @return ftxui::Element - */ - ftxui::Element commitRender(cpplibostree::OSTreeRepo& repo, - const std::vector& visibleCommitMap, - const std::unordered_map& visibleBranches, - const std::unordered_map& branchColorMap, - size_t selectedCommit = 0); +/** + * @brief Creates a Renderer for the commit section. + * + * @param ostreetui OSTreeTUI containing OSTreeRepo and UI info. + * @param branchColorMap Map from branch to its display color. + * @param selectedCommit Commit that should be marked as selected. + * @return UI Element + */ +[[nodiscard]] ftxui::Element commitRender( + OSTreeTUI& ostreetui, + const std::unordered_map& branchColorMap); - /** - * @brief Add a line to a commit-tree-column and commit-info-column. - * Both are built and set using addTreeLine() and addTreeLine(). - * - * @param treeLineType type of commit_tree - * @param lineType type of commit_info (e.g. hash, date,...) - * @param treeElements commit tree column - * @param commElements commit info column - * @param commit commit to render / get info from - * @param highlight should commit be highlighted (as selected) - * @param usedBranches branches to render - * @param branchColorMap branch colors - */ - void addLine(const RenderTree& treeLineType, const RenderLine& lineType, - ftxui::Elements& treeElements, ftxui::Elements& commElements, - const cpplibostree::Commit& commit, - const bool& highlight, - const std::unordered_map& usedBranches, - const std::unordered_map& branchColorMap); +/** + * @brief Builds a commit-tree line. + * + * @param treeLineType Type of commit-tree. + * @param commit Commit to get the information from (to render). + * @param usedBranches Branches, that should be rendered (visible). + * @param branchColorMap Branch colors to use. + * @return UI Element, one commit-tree line. + */ +[[nodiscard]] ftxui::Element addTreeLine( + const RenderTree& treeLineType, + const cpplibostree::Commit& commit, + const std::unordered_map& usedBranches, + const std::unordered_map& branchColorMap); - /** - * @brief build a commit-tree line - * - * @param treeLineType type of commit-tree - * @param commit commit to render / get info from - * @param usedBranches branches to render - * @param branchColorMap branch colors - * @return ftxui::Element commit-tree line - */ - ftxui::Element addTreeLine(const RenderTree& treeLineType, - const cpplibostree::Commit& commit, - const std::unordered_map& usedBranches, - const std::unordered_map& branchColorMap); - - /** - * @brief build a commit-info line - * - * @param lineType type of commit-info - * @param commit commit to render / get info from - * @param highlight should commit be highlighted (as selected) - * @param branchColorMap branch colors - * @return ftxui::Element commit-info line - */ - ftxui::Element addCommLine(RenderLine lineType, - const cpplibostree::Commit& commit, - const bool& highlight, - const std::unordered_map& branchColorMap); - -} // namespace CommitRender +} // namespace CommitRender diff --git a/src/core/footer.cpp b/src/core/footer.cpp index ebb006f..6791033 100644 --- a/src/core/footer.cpp +++ b/src/core/footer.cpp @@ -2,16 +2,21 @@ #include "footer.hpp" -ftxui::Element Footer::footerRender() { - using namespace ftxui; - - return hbox({ - text("OSTree TUI") | bold | hyperlink("https://github.com/AP-Sensing/ostree-tui"), - separator(), - text(content) | (content == DEFAULT_CONTENT ? color(Color::White) : color(Color::YellowLight)), - }); +ftxui::Element Footer::FooterRender() { + using namespace ftxui; + + return hbox({ + text("OSTree TUI") | bold | hyperlink("https://github.com/AP-Sensing/ostree-tui"), + separator(), + text(content) | + (content == DEFAULT_CONTENT ? color(Color::White) : color(Color::YellowLight)), + }); +} + +void Footer::ResetContent() { + content = DEFAULT_CONTENT; } -void Footer::resetContent() { - content = DEFAULT_CONTENT; +void Footer::SetContent(std::string content) { + this->content = content; } diff --git a/src/core/footer.hpp b/src/core/footer.hpp index 289467d..899faed 100644 --- a/src/core/footer.hpp +++ b/src/core/footer.hpp @@ -4,18 +4,22 @@ | keyboard shortcuts info. |___________________________________________________________*/ - class Footer { -public: - const std::string DEFAULT_CONTENT {" || Alt+Q / Esc : Quit || Alt+R : Refresh || Alt+C : Copy commit hash || "}; - std::string content {DEFAULT_CONTENT}; - -public: + public: Footer() = default; - - /// reset footer text to default string - void resetContent(); - /// create a Renderer for the footer section - ftxui::Element footerRender(); + /// @brief Resets footer text to default string. + void ResetContent(); + + /// @brief Creates a Renderer for the footer section. + ftxui::Element FooterRender(); + + // Setter + void SetContent(std::string content); + + private: + const std::string DEFAULT_CONTENT{ + " || Alt+Q : Quit || Alt+R : Refresh || Alt+C : Copy commit hash || Alt+P : Promote " + "Commit "}; + std::string content{DEFAULT_CONTENT}; }; diff --git a/src/core/manager.cpp b/src/core/manager.cpp index a40e294..36faca3 100644 --- a/src/core/manager.cpp +++ b/src/core/manager.cpp @@ -1,213 +1,107 @@ #include "manager.hpp" +#include #include #include -#include #include "ftxui/component/component.hpp" // for Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop -#include "ftxui/component/event.hpp" // for Event -#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border +#include "ftxui/component/event.hpp" // for Event +#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border #include "../util/cpplibostree.hpp" -// Manager - -Manager::Manager(const ftxui::Component& infoView, const ftxui::Component& filterView, const ftxui::Component& promotionView) { - using namespace ftxui; +#include "OSTreeTUI.hpp" - tabSelection = Menu(&tab_entries, &tab_index, MenuOption::HorizontalAnimated()); - - tabContent = Container::Tab({ - infoView, - filterView, - promotionView - }, - &tab_index); - - managerRenderer = Container::Vertical({ - tabSelection, - tabContent - }); -} - -// BranchBoxManager +// Manager -BranchBoxManager::BranchBoxManager(cpplibostree::OSTreeRepo& repo, std::unordered_map& visibleBranches) { +Manager::Manager(OSTreeTUI& ostreetui, + const ftxui::Component& infoView, + const ftxui::Component& filterView) + : ostreetui(ostreetui) { using namespace ftxui; - // branch visibility - for (const auto& branch : repo.getBranches()) { - branchBoxes->Add(Checkbox(branch, &(visibleBranches.at(branch)))); - } + tabSelection = Menu(&tab_entries, &tab_index, MenuOption::HorizontalAnimated()); + + tabContent = Container::Tab({infoView, filterView}, &tab_index); + + managerRenderer = Container::Vertical( + {tabSelection, tabContent, + Renderer([] { return vbox({filler()}) | flex; }), // push elements apart + Renderer([&] { + Elements branches; + for (size_t i{ostreetui.GetColumnToBranchMap().size()}; i > 0; i--) { + std::string branch = ostreetui.GetColumnToBranchMap().at(i - 1); + std::string line = "――☐――― " + branch; + branches.push_back(text(line) | color(ostreetui.GetBranchColorMap().at(branch))); + } + return vbox(branches); + })}); } -ftxui::Element BranchBoxManager::branchBoxRender(){ - using namespace ftxui; - - // branch filter - Elements bfb_elements = { - text(L"branches:") | bold, - filler(), - branchBoxes->Render() | vscroll_indicator | frame | size(HEIGHT, LESS_THAN, 10), - }; - return vbox(bfb_elements); +ftxui::Component Manager::getManagerRenderer() { + return managerRenderer; } -// CommitInfoManager - -ftxui::Element CommitInfoManager::renderInfoView(const cpplibostree::Commit& displayCommit) { - using namespace ftxui; - - // selected commit info - Elements signatures; - for (const auto& signature : displayCommit.signatures) { - std::string ts = std::format("{:%Y-%m-%d %T %Ez}", - std::chrono::time_point_cast(signature.timestamp)); - signatures.push_back(vbox({ - hbox({ - text("‣ "), - text(signature.pubkeyAlgorithm) | bold, - text(" signature") - }), - text(" with key ID " + signature.fingerprint), - text(" made " + ts) - })); - } - return vbox({ - text(" Subject:") | color(Color::Green), - paragraph(displayCommit.subject) | color(Color::White), - filler(), - text(" Hash: ") | color(Color::Green), - text(displayCommit.hash), - filler(), - text(" Date: ") | color(Color::Green), - text(std::format("{:%Y-%m-%d %T %Ez}", - std::chrono::time_point_cast(displayCommit.timestamp))), - filler(), - // TODO insert version, only if exists - displayCommit.version.empty() - ? filler() - : text(" Version: ") | color(Color::Green), - displayCommit.version.empty() - ? filler() - : text(displayCommit.version), - text(" Parent: ") | color(Color::Green), - text(displayCommit.parent), - filler(), - text(" Checksum: ") | color(Color::Green), - text(displayCommit.contentChecksum), - filler(), - displayCommit.signatures.size() > 0 ? text(" Signatures: ") | color(Color::Green) : text(""), - vbox(signatures), - filler() - }); +int Manager::getTabIndex() const { + return tab_index; } -// ContentPromotionManager - -ContentPromotionManager::ContentPromotionManager(bool show_tooltips): show_tooltips(show_tooltips) { - using namespace ftxui; - - subjectComponent = Input(&newSubject, "subject"); -} +// BranchBoxManager -void ContentPromotionManager::setBranchRadiobox(ftxui::Component radiobox) { +BranchBoxManager::BranchBoxManager(OSTreeTUI& ostreetui, + cpplibostree::OSTreeRepo& repo, + std::unordered_map& visibleBranches) { using namespace ftxui; - branchSelection = CatchEvent(radiobox, [&](const Event& event) { - // copy commit id - if (event == Event::Return) { - subjectComponent->TakeFocus(); - } - return false; - }); -} + CheckboxOption cboption = {.on_change = [&] { ostreetui.RefreshCommitListComponent(); }}; -void ContentPromotionManager::setApplyButton(ftxui::Component button) { - applyButton = button; + // branch visibility + for (const auto& branch : repo.getBranches()) { + branchBoxes->Add(Checkbox(branch, &(visibleBranches.at(branch)), cboption)); + } } -ftxui::Elements ContentPromotionManager::renderPromotionCommand(cpplibostree::OSTreeRepo& ostreeRepo, const std::string& selectedCommitHash) { +ftxui::Element BranchBoxManager::branchBoxRender() { using namespace ftxui; - assert(branchSelection); - assert(applyButton); - - Elements line; - line.push_back(text("ostree commit") | bold); - line.push_back(text(" --repo=" + ostreeRepo.getRepoPath()) | bold); - line.push_back(text(" -b " + ostreeRepo.getBranches().at(static_cast(selectedBranch))) | bold); - line.push_back(text(" --keep-metadata") | bold); - // optional subject - if (!newSubject.empty()) { - line.push_back(text(" -s \"") | bold); - line.push_back(text(newSubject) | color(Color::BlueLight) | bold); - line.push_back(text("\"") | bold); - } - // commit - line.push_back(text(" --tree=ref=" + selectedCommitHash) | bold); - - return line; + // branch filter + Elements bfb_elements = { + text(L"branches:") | bold, + filler(), + branchBoxes->Render() | vscroll_indicator | frame | size(HEIGHT, LESS_THAN, 10), + }; + return vbox(bfb_elements); } -ftxui::Component ContentPromotionManager::composePromotionComponent() { - using namespace ftxui; +// CommitInfoManager - return Container::Vertical({ - branchSelection, - Container::Vertical({ - subjectComponent, - applyButton, - }), - }); -} +ftxui::Element CommitInfoManager::renderInfoView(const cpplibostree::Commit& displayCommit) { + using namespace ftxui; -ftxui::Element ContentPromotionManager::renderPromotionView(cpplibostree::OSTreeRepo& ostreeRepo, int screenHeight, const cpplibostree::Commit& displayCommit) { - using namespace ftxui; - - assert(branchSelection); - assert(applyButton); - - // compute screen element sizes - int screenOverhead {8}; // borders, footer, etc. - int commitWinHeight {3}; - int apsectWinHeight {8}; - int tooltipsWinHeight {2}; - int branchSelectWinHeight = screenHeight - screenOverhead - commitWinHeight - apsectWinHeight - tooltipsWinHeight; - // tooltips only get shown, if the window is sufficiently large - if (branchSelectWinHeight < 4) { - tooltipsWinHeight = 0; - branchSelectWinHeight = 4; - } - - // build elements - auto commitHashElem = vbox({text(" Commit: ") | bold | color(Color::Green), text(" " + displayCommit.hash)}) | flex; - auto branchWin = window(text("New Branch"), branchSelection->Render() | vscroll_indicator | frame); - auto subjectWin = window(text("Subject"), subjectComponent->Render()) | flex; - auto applyButtonWin = applyButton->Render() | color(Color::Green) | size(WIDTH, GREATER_THAN, 9) | flex; - - auto toolTipContent = [&](size_t tip) { - return vbox({ - separatorCharacter("⎯"), - text(" 🛈 " + tool_tip_strings.at(tip)), - }); - }; - auto toolTipsWin = !show_tooltips || tooltipsWinHeight < 2 ? filler() : // only show if screen is reasonable size - branchSelection->Focused() ? toolTipContent(0) : - subjectComponent->Focused() ? toolTipContent(1) : - applyButton->Focused() ? toolTipContent(2) : - filler(); - - // build element composition - return vbox({ - commitHashElem | size(HEIGHT, EQUAL, commitWinHeight), - branchWin | size(HEIGHT, LESS_THAN, branchSelectWinHeight), - vbox({ - subjectWin, - applyButtonWin, - }) | flex | size(HEIGHT, LESS_THAN, apsectWinHeight), - hflow(renderPromotionCommand(ostreeRepo, displayCommit.hash)) | flex_grow, - filler(), - toolTipsWin, - }) | flex_grow; + // selected commit info + Elements signatures; + for (const auto& signature : displayCommit.signatures) { + std::string ts = + std::format("{:%Y-%m-%d %T %Ez}", + std::chrono::time_point_cast(signature.timestamp)); + signatures.push_back( + vbox({hbox({text("‣ "), text(signature.pubkeyAlgorithm) | bold, text(" signature")}), + text(" with key ID " + signature.fingerprint), text(" made " + ts)})); + } + return vbox( + {text(" Subject:") | color(Color::Green), + paragraph(displayCommit.subject) | color(Color::White), filler(), + text(" Hash: ") | color(Color::Green), text(displayCommit.hash), filler(), + text(" Date: ") | color(Color::Green), + text(std::format("{:%Y-%m-%d %T %Ez}", std::chrono::time_point_cast( + displayCommit.timestamp))), + filler(), + // TODO insert version, only if exists + displayCommit.version.empty() ? filler() : text(" Version: ") | color(Color::Green), + displayCommit.version.empty() ? filler() : text(displayCommit.version), + text(" Parent: ") | color(Color::Green), text(displayCommit.parent), filler(), + text(" Checksum: ") | color(Color::Green), text(displayCommit.contentChecksum), filler(), + displayCommit.signatures.size() > 0 ? text(" Signatures: ") | color(Color::Green) + : text(""), + vbox(signatures), filler()}); } diff --git a/src/core/manager.hpp b/src/core/manager.hpp index 4246214..12ac40d 100644 --- a/src/core/manager.hpp +++ b/src/core/manager.hpp @@ -1,10 +1,9 @@ /*_____________________________________________________________ | Manager Render | Right portion of main window, includes branch filter & - | detailed commit info of the selected commit. In future - | different modes should be supported (like a rebase mode - | exchangeable with the commit info) + | detailed commit info of the selected commit. |___________________________________________________________*/ +#pragma once #include #include @@ -13,106 +12,56 @@ #include "../util/cpplibostree.hpp" +class OSTreeTUI; + /// Interchangeable View class Manager { -public: - int tab_index{0}; + public: + Manager(OSTreeTUI& ostreetui, + const ftxui::Component& infoView, + const ftxui::Component& filterView); - std::vector tab_entries = { - " Info ", " Filter ", " Promote " - }; + private: + OSTreeTUI& ostreetui; - ftxui::Component tabSelection; - ftxui::Component tabContent; + int tab_index{0}; + std::vector tab_entries = {" Info ", " Filter "}; // because the combination of all interchangeable views is very simple, - // we can (in contrast to the other ones) render this one immediately + // we can (in contrast to the other ones) render this one here ftxui::Component managerRenderer; + ftxui::Component tabSelection; + ftxui::Component tabContent; -public: - Manager(const ftxui::Component& infoView, const ftxui::Component& filterView, const ftxui::Component& promotionView); + public: + ftxui::Component getManagerRenderer(); + int getTabIndex() const; }; class CommitInfoManager { -public: + public: /** * @brief Build the info view Element. - * + * * @param displayCommit Commit to display the information of. - * @return ftxui::Element + * @return ftxui::Element */ - static ftxui::Element renderInfoView(const cpplibostree::Commit& displayCommit); + [[nodiscard]] static ftxui::Element renderInfoView(const cpplibostree::Commit& displayCommit); }; class BranchBoxManager { -public: - ftxui::Component branchBoxes = ftxui::Container::Vertical({}); + public: + BranchBoxManager(OSTreeTUI& ostreetui, + cpplibostree::OSTreeRepo& repo, + std::unordered_map& visibleBranches); -public: - BranchBoxManager(cpplibostree::OSTreeRepo& repo, std::unordered_map& visibleBranches); - /** * @brief Build the branch box Element. - * - * @return ftxui::Element - */ - ftxui::Element branchBoxRender(); -}; - -class ContentPromotionManager { -public: - // branch selection - ftxui::Component branchSelection; // must be set from OSTreeTUI - int selectedBranch{0}; - - // subject - ftxui::Component subjectComponent; - - std::string newSubject{""}; - - // apply button - ftxui::Component applyButton; // must be set from OSTreeTUI - - // tool-tips - bool show_tooltips{true}; - ftxui::Component tool_tips_comp; - const std::vector tool_tip_strings = { - "Branch to promote the Commit to.", - "New subject for promoted Commit (optional).", - "Apply the Commit Promotion (write to repository).", - }; - -public: - /** - * @brief Constructor for the Promotion Manager. - * - * @warning The branchSelection and applyButton have to be set - * using the respective set-methods AFTER construction, as they - * have to be constructed in the OSTreeTUI::main - */ - ContentPromotionManager(bool show_tooltips = true); - - /// Setter - void setBranchRadiobox(ftxui::Component radiobox); - /// Setter - void setApplyButton(ftxui::Component button); - - /** - * @brief Build the promotion view Component - * - * @warning branchSelection & applyButton have to be set first (checked through assert) - * @return ftxui::Component - */ - ftxui::Component composePromotionComponent(); - - /// renders the promotion command resulting from the current user settings (ostree commit ...) - ftxui::Elements renderPromotionCommand(cpplibostree::OSTreeRepo& ostreeRepo, const std::string& selectedCommitHash); - - /** - * @brief Build the promotion view Element - * - * @warning branchSelection & applyButton have to be set first (checked through assert) + * * @return ftxui::Element */ - ftxui::Element renderPromotionView(cpplibostree::OSTreeRepo& ostreeRepo, int screenHeight, const cpplibostree::Commit& displayCommit); + [[nodiscard]] ftxui::Element branchBoxRender(); + + public: + ftxui::Component branchBoxes = ftxui::Container::Vertical({}); }; diff --git a/src/core/scroller.cpp b/src/core/scroller.cpp deleted file mode 100644 index 71e8b17..0000000 --- a/src/core/scroller.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "scroller.hpp" - -#include // for max, min -#include // for Component, ComponentBase -#include // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp -#include // for move - -#include "ftxui/component/component.hpp" // for Make -#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::WheelDown, Mouse::WheelUp -#include "ftxui/dom/deprecated.hpp" // for text -#include "ftxui/dom/elements.hpp" // for operator|, Element, size, vbox, EQUAL, HEIGHT, dbox, reflect, focus, inverted, nothing, select, vscroll_indicator, yflex, yframe -#include "ftxui/dom/node.hpp" // for Node -#include "ftxui/dom/requirement.hpp" // for Requirement -#include "ftxui/screen/box.hpp" // for Box - - -/* - * This Scroller Element is a modified version of the Scroller in the following repository - * Title: git-tui - * Author: Arthur Sonzogni - * Date: 2021 - * Availability: https://github.com/ArthurSonzogni/git-tui/blob/master/src/scroller.cpp - */ -namespace ftxui { - -class ScrollerBase : public ComponentBase { - public: - ScrollerBase(size_t *selectedCommit, size_t elementLength, Component child): - sc(selectedCommit), - elLength(elementLength) { - Add(child); - } - - private: - size_t *sc{nullptr}; - size_t elLength{1}; - - Element Render() final { - auto focused = Focused() ? focus : ftxui::select; - - Element background = ComponentBase::Render(); - background->ComputeRequirement(); - size_ = background->requirement().min_y; - return dbox({ - std::move(background), - vbox({ - // TODO change *4 for dynamic height, or make height stay the same - text(L"") | size(HEIGHT, EQUAL, static_cast(*sc * elLength)), - text(L"") | focused, - }), - }) | vscroll_indicator | yframe | yflex | reflect(box_); - } - - bool Focusable() const final { return true; } - - int size_ = 0; - Box box_{}; -}; - -Component Scroller(size_t *selectedCommit, size_t elementLength, Component child) { - return Make(selectedCommit, elementLength, std::move(child)); -} - -} // namespace ftxui diff --git a/src/core/scroller.hpp b/src/core/scroller.hpp deleted file mode 100644 index cdd29f3..0000000 --- a/src/core/scroller.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef SCROLLER_H -#define SCROLLER_H - -#include - -#include "ftxui/component/component_base.hpp" // for Component - -namespace ftxui { - /** - * @brief This Scroller Element is a modified version of the Scroller in the following repository: - * Title: git-tui - * Author: Arthur Sonzogni - * Date: 2021 - * Availability: https://github.com/ArthurSonzogni/git-tui/blob/master/src/scroller.cpp - * - * @param selectedCommit - * @param child - * @return Component - */ - Component Scroller(size_t *selectedCommit, size_t elementLength, Component child); - -} // namespace ftxui -#endif /* end of include guard: SCROLLER_H */ diff --git a/src/main.cpp b/src/main.cpp index 1210dcb..98f5efd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,57 +6,57 @@ /** * @brief Parse all options listed behind an argument - * + * * @param args argument list * @param find argument (as vector for multiple arg version support, like -h & --help) * @return vector of options */ -std::vector getArgOptions(const std::vector& args, const std::vector& find) { - std::vector out; - for (const auto& arg : find) { - auto argIndex = std::find(args.begin(), args.end(), arg); - if (argIndex == args.end()) { - continue; - } - while (++argIndex != args.end()) { - if ((*argIndex).at(0) == '-') { - // arrived at next argument -> stop - break; - } - out.push_back(*argIndex); - } - } - return out; +std::vector getArgOptions(const std::vector& args, + const std::vector& find) { + std::vector out; + for (const auto& arg : find) { + auto argIndex = std::find(args.begin(), args.end(), arg); + if (argIndex == args.end()) { + continue; + } + while (++argIndex != args.end()) { + if ((*argIndex).at(0) == '-') { + // arrived at next argument -> stop + break; + } + out.push_back(*argIndex); + } + } + return out; } /// Check if argument exists in argument list bool argExists(const std::vector& args, std::string arg) { - return std::find(args.begin(), args.end(), arg) != args.end(); + return std::find(args.begin(), args.end(), arg) != args.end(); } /// main for argument parsing and OSTree TUI call int main(int argc, const char** argv) { - // too few amount of arguments - if (argc <= 1) { - return OSTreeTUI::showHelp(argc <= 0 ? "ostree" : argv[0], "no repository provided"); - } + // too few amount of arguments + if (argc <= 1) { + return OSTreeTUI::showHelp(argc <= 0 ? "ostree" : argv[0], "no repository provided"); + } - std::vector args(argv + 1, argv + argc); - // -h, --help - if (argExists(args,"-h") || argExists(args,"--help")) { - return OSTreeTUI::showHelp(argv[0]); - } - // -v, --version - if (argExists(args,"-v") || argExists(args,"--version")) { - return OSTreeTUI::showVersion(); - } - // assume ostree repository path as first argument - std::string repo = args.at(0); - // -r, --refs - std::vector startupBranches = getArgOptions(args, {"-r", "--refs"}); - // -n, --no-tooltips - bool hideTooltips = argExists(args, "-n") || argExists(args, "--no-tooltips"); + std::vector args(argv + 1, argv + argc); + // -h, --help + if (argExists(args, "-h") || argExists(args, "--help")) { + return OSTreeTUI::showHelp(argv[0]); + } + // -v, --version + if (argExists(args, "-v") || argExists(args, "--version")) { + return OSTreeTUI::showVersion(); + } + // assume ostree repository path as first argument + std::string repo = args.at(0); + // -r, --refs + std::vector startupBranches = getArgOptions(args, {"-r", "--refs"}); - // OSTree TUI - return OSTreeTUI::main(repo, startupBranches, !hideTooltips); + // OSTree TUI + OSTreeTUI ostreetui(repo, startupBranches); + return ostreetui.Run(); } diff --git a/src/util/cpplibostree.cpp b/src/util/cpplibostree.cpp index 57389a3..9673cdd 100644 --- a/src/util/cpplibostree.cpp +++ b/src/util/cpplibostree.cpp @@ -8,329 +8,338 @@ #include #include // C -#include +#include #include #include #include -#include +#include namespace cpplibostree { - OSTreeRepo::OSTreeRepo(std::string path): - repoPath(std::move(path)), - commitList({}), - branches({}) { - updateData(); +OSTreeRepo::OSTreeRepo(std::string path) : repoPath(std::move(path)), commitList({}), branches({}) { + updateData(); +} + +bool OSTreeRepo::updateData() { + // parse branches + std::string branchString = getBranchesAsString(); + std::stringstream bss(branchString); + std::string word; + while (bss >> word) { + branches.push_back(word); } - bool OSTreeRepo::updateData() { - // parse branches - std::string branchString = getBranchesAsString(); - std::stringstream bss(branchString); - std::string word; - while (bss >> word) { - branches.push_back(word); - } + // parse commits + commitList = parseCommitsAllBranches(); - // parse commits - commitList = parseCommitsAllBranches(); - - return true; - } + return true; +} - // METHODS +// METHODS - OstreeRepo* OSTreeRepo::_c() { - // open repo - GError *error{nullptr}; - OstreeRepo *repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); - if (repo == nullptr) { - g_printerr("Error opening repository: %s\n", error->message); - g_error_free(error); - return nullptr; - } - return repo; +OstreeRepo* OSTreeRepo::_c() { + // open repo + GError* error{nullptr}; + OstreeRepo* repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); + if (repo == nullptr) { + g_printerr("Error opening repository: %s\n", error->message); + g_error_free(error); + return nullptr; } - - std::string OSTreeRepo::getRepoPath() { - return repoPath; + return repo; +} + +const std::string& OSTreeRepo::getRepoPath() const { + return repoPath; +} + +const CommitList& OSTreeRepo::getCommitList() const { + return commitList; +} + +const std::vector& OSTreeRepo::getBranches() const { + return branches; +} + +bool OSTreeRepo::isCommitSigned(const Commit& commit) { + return commit.signatures.size() > 0; +} + +Commit OSTreeRepo::parseCommit(GVariant* variant, + const std::string& branch, + const std::string& hash) { + Commit commit; + + const gchar* subject{nullptr}; + const gchar* body{nullptr}; + const gchar* version{nullptr}; + guint64 timestamp{0}; + g_autofree char* parent{nullptr}; + + // see OSTREE_COMMIT_GVARIANT_FORMAT + g_variant_get(variant, "(a{sv}aya(say)&s&stayay)", nullptr, nullptr, nullptr, &subject, &body, + ×tamp, nullptr, nullptr); + assert(body); + assert(timestamp); + + // timestamp + timestamp = GUINT64_FROM_BE(timestamp); + commit.timestamp = Timepoint(std::chrono::seconds(timestamp)); + + // parent + parent = ostree_commit_get_parent(variant); + if (parent) { + commit.parent = parent; + } else { + commit.parent = "(no parent)"; } - CommitList OSTreeRepo::getCommitList() { - return commitList; + // content checksum + g_autofree char* contents = ostree_commit_get_content_checksum(variant); + assert(contents); + commit.contentChecksum = contents; + + // version + g_autoptr(GVariant) metadata = NULL; + const char* ret = NULL; + metadata = g_variant_get_child_value(variant, 0); + if (g_variant_lookup(metadata, OSTREE_COMMIT_META_KEY_VERSION, "&s", &ret)) { + version = g_strdup(ret); + commit.version = version; } - std::vector OSTreeRepo::getBranches() { - return branches; + // subject + if (subject[0]) { + std::string val = subject; + commit.subject = val; + } else { + commit.subject = "(no subject)"; } - bool OSTreeRepo::isCommitSigned(const Commit& commit) { - return commit.signatures.size() > 0; + // body + if (body[0]) { + commit.body = body; } - Commit OSTreeRepo::parseCommit(GVariant *variant, const std::string& branch, const std::string& hash) { - Commit commit; - - const gchar *subject {nullptr}; - const gchar *body {nullptr}; - const gchar *version {nullptr}; - guint64 timestamp {0}; - g_autofree char *parent {nullptr}; - - // see OSTREE_COMMIT_GVARIANT_FORMAT - g_variant_get(variant, "(a{sv}aya(say)&s&stayay)", nullptr, nullptr, nullptr, &subject, &body, ×tamp, nullptr, nullptr); - assert(body); - assert(timestamp); - - // timestamp - timestamp = GUINT64_FROM_BE(timestamp); - commit.timestamp = Timepoint(std::chrono::seconds(timestamp)); - - // parent - parent = ostree_commit_get_parent(variant); - if (parent) { - commit.parent = parent; - } else { - commit.parent = "(no parent)"; - } - - // content checksum - g_autofree char *contents = ostree_commit_get_content_checksum(variant); - assert(contents); - commit.contentChecksum = contents; - - // version - g_autoptr (GVariant) metadata = NULL; - const char *ret = NULL; - metadata = g_variant_get_child_value(variant, 0); - if (g_variant_lookup(metadata, OSTREE_COMMIT_META_KEY_VERSION, "&s", &ret)) { - version = g_strdup(ret); - commit.version = version; - } - - // subject - if (subject[0]) { - std::string val = subject; - commit.subject = val; - } else { - commit.subject = "(no subject)"; - } - - // body - if (body[0]) { - commit.body = body; - } - - commit.branch = branch; - commit.hash = hash; - - // Signatures ___ refactor into own method - // open repo - GError *error = nullptr; - OstreeRepo *repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); - if (repo == nullptr) { - g_printerr("Error opening repository: %s\n", error->message); - g_error_free(error); - assert(repo); - } - // see ostree print_object for reference - g_autoptr(OstreeGpgVerifyResult) result = nullptr; - g_autoptr(GError) local_error = nullptr; - result = ostree_repo_verify_commit_ext (repo, commit.hash.c_str(), nullptr, nullptr, nullptr, - &local_error); - if (g_error_matches (local_error, OSTREE_GPG_ERROR, OSTREE_GPG_ERROR_NO_SIGNATURE) || local_error != nullptr) { - /* Ignore */ - } else { - assert(result); - guint n_sigs = ostree_gpg_verify_result_count_all (result); - // parse all found signatures - for (guint ii = 0; ii < n_sigs; ii++) { - g_autoptr(GVariant) variant = nullptr; - variant = ostree_gpg_verify_result_get_all (result, ii); - // see ostree_gpg_verify_result_describe_variant for reference - gint64 timestamp {0}; - gint64 exp_timestamp {0}; - gint64 key_exp_timestamp {0}; - gint64 key_exp_timestamp_primary{0}; - const char *fingerprint {nullptr}; - const char *fingerprintPrimary {nullptr}; - const char *pubkey_algo {nullptr}; - const char *user_name {nullptr}; - const char *user_email {nullptr}; - gboolean valid {false}; - gboolean sigExpired {false}; - gboolean keyExpired {false}; - gboolean keyRevoked {false}; - gboolean keyMissing {false}; - - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_VALID, "b", &valid); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_SIG_EXPIRED, "b", &sigExpired); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_EXPIRED, "b", &keyExpired); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_REVOKED, "b", &keyRevoked); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_MISSING, "b", &keyMissing); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_FINGERPRINT, "&s", &fingerprint); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_FINGERPRINT_PRIMARY, "&s", &fingerprintPrimary); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_TIMESTAMP, "x", ×tamp); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_EXP_TIMESTAMP, "x", &exp_timestamp); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_PUBKEY_ALGO_NAME, "&s", &pubkey_algo); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_USER_NAME, "&s", &user_name); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_USER_EMAIL, "&s", &user_email); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_EXP_TIMESTAMP, "x", &key_exp_timestamp); - g_variant_get_child (variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_EXP_TIMESTAMP_PRIMARY, "x", &key_exp_timestamp_primary); - - // create signature struct - Signature sig; - - sig.valid = valid; - sig.sigExpired = sigExpired; - sig.keyExpired = keyExpired; - sig.keyRevoked = keyRevoked; - sig.keyMissing = keyMissing; - sig.fingerprint = fingerprint; - sig.fingerprintPrimary = fingerprintPrimary; - sig.timestamp = Timepoint(std::chrono::seconds(timestamp)); - sig.expireTimestamp = Timepoint(std::chrono::seconds(exp_timestamp)); - sig.pubkeyAlgorithm = pubkey_algo; - sig.username = user_name; - sig.usermail = user_email; - sig.keyExpireTimestamp = Timepoint(std::chrono::seconds(key_exp_timestamp)); - sig.keyExpireTimestampPrimary = Timepoint(std::chrono::seconds(key_exp_timestamp_primary)); - - commit.signatures.push_back(std::move(sig)); - } + commit.branch = branch; + commit.hash = hash; + + // Signatures ___ refactor into own method + // open repo + GError* error = nullptr; + OstreeRepo* repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); + if (repo == nullptr) { + g_printerr("Error opening repository: %s\n", error->message); + g_error_free(error); + assert(repo); + } + // see ostree print_object for reference + g_autoptr(OstreeGpgVerifyResult) result = nullptr; + g_autoptr(GError) local_error = nullptr; + result = ostree_repo_verify_commit_ext(repo, commit.hash.c_str(), nullptr, nullptr, nullptr, + &local_error); + if (g_error_matches(local_error, OSTREE_GPG_ERROR, OSTREE_GPG_ERROR_NO_SIGNATURE) || + local_error != nullptr) { + /* Ignore */ + } else { + assert(result); + guint n_sigs = ostree_gpg_verify_result_count_all(result); + // parse all found signatures + for (guint ii = 0; ii < n_sigs; ii++) { + g_autoptr(GVariant) variant = nullptr; + variant = ostree_gpg_verify_result_get_all(result, ii); + // see ostree_gpg_verify_result_describe_variant for reference + gint64 timestamp{0}; + gint64 exp_timestamp{0}; + gint64 key_exp_timestamp{0}; + gint64 key_exp_timestamp_primary{0}; + const char* fingerprint{nullptr}; + const char* fingerprintPrimary{nullptr}; + const char* pubkey_algo{nullptr}; + const char* user_name{nullptr}; + const char* user_email{nullptr}; + gboolean valid{false}; + gboolean sigExpired{false}; + gboolean keyExpired{false}; + gboolean keyRevoked{false}; + gboolean keyMissing{false}; + + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_VALID, "b", &valid); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_SIG_EXPIRED, "b", &sigExpired); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_EXPIRED, "b", &keyExpired); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_REVOKED, "b", &keyRevoked); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_MISSING, "b", &keyMissing); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_FINGERPRINT, "&s", &fingerprint); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_FINGERPRINT_PRIMARY, "&s", + &fingerprintPrimary); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_TIMESTAMP, "x", ×tamp); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_EXP_TIMESTAMP, "x", + &exp_timestamp); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_PUBKEY_ALGO_NAME, "&s", + &pubkey_algo); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_USER_NAME, "&s", &user_name); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_USER_EMAIL, "&s", &user_email); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_EXP_TIMESTAMP, "x", + &key_exp_timestamp); + g_variant_get_child(variant, OSTREE_GPG_SIGNATURE_ATTR_KEY_EXP_TIMESTAMP_PRIMARY, "x", + &key_exp_timestamp_primary); + + // create signature struct + Signature sig; + + sig.valid = valid; + sig.sigExpired = sigExpired; + sig.keyExpired = keyExpired; + sig.keyRevoked = keyRevoked; + sig.keyMissing = keyMissing; + sig.fingerprint = fingerprint; + sig.fingerprintPrimary = fingerprintPrimary; + sig.timestamp = Timepoint(std::chrono::seconds(timestamp)); + sig.expireTimestamp = Timepoint(std::chrono::seconds(exp_timestamp)); + sig.pubkeyAlgorithm = pubkey_algo; + sig.username = user_name; + sig.usermail = user_email; + sig.keyExpireTimestamp = Timepoint(std::chrono::seconds(key_exp_timestamp)); + sig.keyExpireTimestampPrimary = + Timepoint(std::chrono::seconds(key_exp_timestamp_primary)); + + commit.signatures.push_back(std::move(sig)); } + } - return commit; + return commit; +} + +// modified log_commit() from +// https://github.com/ostreedev/ostree/blob/main/src/ostree/ot-builtin-log.c#L40 +gboolean OSTreeRepo::parseCommitsRecursive(OstreeRepo* repo, + const gchar* checksum, + GError** error, + CommitList* commitList, + const std::string& branch, + gboolean isRecurse) { + GError* local_error{nullptr}; + + g_autoptr(GVariant) variant = nullptr; + if (!ostree_repo_load_variant(repo, OSTREE_OBJECT_TYPE_COMMIT, checksum, &variant, + &local_error)) { + return isRecurse && g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); } - // modified log_commit() from https://github.com/ostreedev/ostree/blob/main/src/ostree/ot-builtin-log.c#L40 - gboolean OSTreeRepo::parseCommitsRecursive (OstreeRepo *repo, const gchar *checksum, GError **error, - CommitList *commitList, const std::string& branch, gboolean isRecurse) { - GError *local_error{nullptr}; + commitList->insert({static_cast(checksum), + parseCommit(variant, branch, static_cast(checksum))}); - g_autoptr (GVariant) variant = nullptr; - if (!ostree_repo_load_variant(repo, OSTREE_OBJECT_TYPE_COMMIT, checksum, &variant, &local_error)) { - return isRecurse && g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); - } + // parent recursion + g_autofree char* parent = ostree_commit_get_parent(variant); - commitList->insert({ - static_cast(checksum), - parseCommit(variant, branch, static_cast(checksum)) - }); + return !(parent && !parseCommitsRecursive(repo, parent, error, commitList, branch, true)); +} - // parent recursion - g_autofree char *parent = ostree_commit_get_parent(variant); +CommitList OSTreeRepo::parseCommitsOfBranch(const std::string& branch) { + auto ret = CommitList(); - return !(parent && !parseCommitsRecursive(repo, parent, error, commitList, branch, true)); + // open repo + GError* error = nullptr; + OstreeRepo* repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); + if (repo == nullptr) { + g_printerr("Error opening repository: %s\n", error->message); + g_error_free(error); + return ret; } - CommitList OSTreeRepo::parseCommitsOfBranch(const std::string& branch) { - auto ret = CommitList(); + // recursive commit log + g_autofree char* checksum = nullptr; + if (!ostree_repo_resolve_rev(repo, branch.c_str(), false, &checksum, &error)) { + return ret; + } - // open repo - GError *error = nullptr; - OstreeRepo *repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); - if (repo == nullptr) { - g_printerr("Error opening repository: %s\n", error->message); - g_error_free(error); - return ret; - } + parseCommitsRecursive(repo, checksum, &error, &ret, branch); - // recursive commit log - g_autofree char *checksum = nullptr; - if (!ostree_repo_resolve_rev(repo, branch.c_str(), false, &checksum, &error)) { - return ret; - } + return ret; +} - parseCommitsRecursive(repo, checksum, &error, &ret, branch); +CommitList OSTreeRepo::parseCommitsAllBranches() { + std::istringstream branches_string(getBranchesAsString()); + std::string branch; - return ret; - } + CommitList commits_all_branches; - CommitList OSTreeRepo::parseCommitsAllBranches() { + while (branches_string >> branch) { + auto commits = parseCommitsOfBranch(branch); + commits_all_branches.insert(commits.begin(), commits.end()); + } - std::istringstream branches_string(getBranchesAsString()); - std::string branch; + return commits_all_branches; +} - CommitList commits_all_branches; +std::string OSTreeRepo::getBranchesAsString() { + std::string branches_str; - while (branches_string >> branch) { - auto commits = parseCommitsOfBranch(branch); - commits_all_branches.insert(commits.begin(), commits.end()); - } + // open repo + GError* error{nullptr}; + OstreeRepo* repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); - return commits_all_branches; + if (repo == nullptr) { + g_printerr("Error opening repository: %s\n", error->message); + g_error_free(error); + return ""; } - std::string OSTreeRepo::getBranchesAsString() { - std::string branches_str; - - // open repo - GError *error {nullptr}; - OstreeRepo *repo = ostree_repo_open_at(AT_FDCWD, repoPath.c_str(), nullptr, &error); + // get a list of refs + GHashTable* refs_hash{nullptr}; + gboolean result = ostree_repo_list_refs_ext(repo, nullptr, &refs_hash, + OSTREE_REPO_LIST_REFS_EXT_NONE, nullptr, &error); + if (!result) { + g_printerr("Error listing refs: %s\n", error->message); + g_error_free(error); + g_object_unref(repo); + // TODO exit with error + return ""; + } - if (repo == nullptr) { - g_printerr("Error opening repository: %s\n", error->message); - g_error_free(error); - return ""; - } + // iterate through the refs + GHashTableIter iter; + gpointer key{nullptr}; + gpointer value{nullptr}; + g_hash_table_iter_init(&iter, refs_hash); + while (g_hash_table_iter_next(&iter, &key, &value)) { + const gchar* ref_name = static_cast(key); + branches_str += " " + std::string(ref_name); + } - // get a list of refs - GHashTable *refs_hash {nullptr}; - gboolean result = ostree_repo_list_refs_ext(repo, nullptr, &refs_hash, OSTREE_REPO_LIST_REFS_EXT_NONE, nullptr, &error); - if (!result) { - g_printerr("Error listing refs: %s\n", error->message); - g_error_free(error); - g_object_unref(repo); - // TODO exit with error - return ""; - } + // free + g_hash_table_unref(refs_hash); - // iterate through the refs - GHashTableIter iter; - gpointer key {nullptr}; - gpointer value {nullptr}; - g_hash_table_iter_init(&iter, refs_hash); - while (g_hash_table_iter_next(&iter, &key, &value)) { - const gchar *ref_name = static_cast(key); - branches_str += " " + std::string(ref_name); - } + return branches_str; +} - // free - g_hash_table_unref(refs_hash); +/// TODO This implementation should not rely on the ostree CLI -> change to libostree usage. +bool OSTreeRepo::PromoteCommit(const std::string& hash, + const std::string& newRef, + const std::vector addMetadataStrings, + const std::string& newSubject, + bool keepMetadata) { + if (hash.size() <= 0 || newRef.size() <= 0) { + return false; + } - return branches_str; + std::string command = "ostree commit"; + command += " --repo=" + repoPath; + command += " -b " + newRef; + command += (newSubject.size() <= 0 ? "" : " -s \"" + newSubject + "\""); + command += (keepMetadata ? "" : " --keep-metadata"); + for (auto str : addMetadataStrings) { + command += " --add-metadata-string=\"" + str + "\""; } + command += " --tree=ref=" + hash; - /// TODO This implementation should not rely on the ostree CLI -> change to libostree usage. - bool OSTreeRepo::promoteCommit(const std::string& hash, const std::string& newRef, - const std::vector addMetadataStrings, - const std::string& newSubject, bool keepMetadata) { - if (hash.size() <= 0 || newRef.size() <= 0) { - return false; - } - - std::string command = "ostree commit"; - command += " --repo=" + repoPath; - command += " -b " + newRef; - command += (newSubject.size() <= 0 - ? "" - : " -s \"" + newSubject + "\""); - command += (keepMetadata - ? "" - : " --keep-metadata"); - for (auto str : addMetadataStrings) { - command += "--add-metadata-string=\"" + str + "\""; - } - command += " --tree=ref=" + hash; - - // run as CLI command - std::unique_ptr pipe(popen(command.c_str(), "r"), pclose); - - if (!pipe) { - return false; - } - return true; + // run as CLI command + std::unique_ptr pipe(popen(command.c_str(), "r"), pclose); + + if (!pipe) { + return false; } + return true; +} -} // namespace cpplibostree +} // namespace cpplibostree diff --git a/src/util/cpplibostree.hpp b/src/util/cpplibostree.hpp index d70b3d0..d10b869 100644 --- a/src/util/cpplibostree.hpp +++ b/src/util/cpplibostree.hpp @@ -13,176 +13,183 @@ #pragma once // C++ +#include #include #include -#include #include #include // C +#include #include #include -#include // external #include #include - namespace cpplibostree { - using Clock = std::chrono::utc_clock; - using Timepoint = std::chrono::time_point; - - struct Signature { - bool valid {false}; - bool sigExpired{true}; - bool keyExpired{true}; - bool keyRevoked{false}; - bool keyMissing{true}; - std::string fingerprint; - std::string fingerprintPrimary; - Timepoint timestamp; - Timepoint expireTimestamp; - std::string pubkeyAlgorithm; - std::string username; - std::string usermail; - Timepoint keyExpireTimestamp; - Timepoint keyExpireTimestampPrimary; - } __attribute__((aligned(128))); - - struct Commit { - std::string hash; - std::string contentChecksum; - std::string subject{"OSTree TUI Error - invalid commit state"}; - std::string body; - std::string version; - Timepoint timestamp; - std::string parent; - std::string branch; - std::vector signatures; - } __attribute__((aligned(128))); - - // map commit hash to commit - using CommitList = std::unordered_map; +using Clock = std::chrono::utc_clock; +using Timepoint = std::chrono::time_point; + +struct Signature { + bool valid{false}; + bool sigExpired{true}; + bool keyExpired{true}; + bool keyRevoked{false}; + bool keyMissing{true}; + std::string fingerprint; + std::string fingerprintPrimary; + Timepoint timestamp; + Timepoint expireTimestamp; + std::string pubkeyAlgorithm; + std::string username; + std::string usermail; + Timepoint keyExpireTimestamp; + Timepoint keyExpireTimestampPrimary; +} __attribute__((aligned(128))); + +struct Commit { + std::string hash; + std::string contentChecksum; + std::string subject{"OSTree TUI Error - invalid commit state"}; + std::string body; + std::string version; + Timepoint timestamp; + std::string parent; + std::string branch; + std::vector signatures; +} __attribute__((aligned(128))); + +// map commit hash to commit +using CommitList = std::unordered_map; + +/** + * @brief OSTreeRepo functions as a C++ wrapper around libostree's OstreeRepo. + * The complete OSTree repository gets parsed into a complete commit list in + * commitList and a list of refs in branches. + */ +class OSTreeRepo { + private: + std::string repoPath; + CommitList commitList; + std::vector branches; + + public: + /** + * @brief Construct a new OSTreeRepo. + * + * @param repoPath Path to the OSTree Repository + */ + explicit OSTreeRepo(std::string repoPath); + + /** + * @brief Return a C-style pointer to a libostree OstreeRepo. This exists, to be + * able to access functions, that have not yet been adapted in this C++ wrapper. + * + * @return OstreeRepo* + */ + OstreeRepo* _c(); + + /// Getter + const std::string& getRepoPath() const; + /// Getter + const CommitList& getCommitList() const; + /// Getter + const std::vector& getBranches() const; + + // Methods + + /** + * @brief Reload the OSTree repository data. + * + * @return true if data was changed during the reload + * @return false if nothing changed + */ + bool updateData(); + + /** + * @brief Check if a certain commit is signed. This simply accesses the + * size() of commit.signatures. + * + * @param commit + * @return true if the commit is signed + * @return false if the commit is not signed + */ + static bool isCommitSigned(const Commit& commit); + + /** + * @brief Parse commits from a ostree log output to a commitList, mapping + * the hashes to commits. + * + * @param branch + * @return std::unordered_map + */ + CommitList parseCommitsOfBranch(const std::string& branch); + + /** + * @brief Performs parseCommitsOfBranch() on all available branches and + * merges all commit lists into one. + * + * @return std::unordered_map + */ + CommitList parseCommitsAllBranches(); + + // read & write access to OSTree repo: + + /** + * @brief Promotes a commit to another branch. Similar to: + * `ostree commit --repo=repo -b newRef -s newSubject --tree=ref=hash` + * + * @param hash hash of the commit to promote + * @param newRef branch to promote to + * @param addMetadataStrings list of metadata strings to add -> KEY=VALUE + * @param newSubject new commit subject, it needed + * @param keepMetadata should new commit keep metadata of old commit + * @return true on success + * @return false on failed promotion + */ + bool PromoteCommit(const std::string& hash, + const std::string& newRef, + const std::vector addMetadataStrings, + const std::string& newSubject = "", + bool keepMetadata = true); + + private: + /** + * @brief Get all branches as a single string, separated by spaces. + * + * @return std::string All branch names, separated by spaces + */ + std::string getBranchesAsString(); + + /** + * @brief Parse a libostree GVariant commit to a C++ commit struct. + * + * @param variant pointer to GVariant commit + * @param branch branch of the commit + * @param hash commit hash + * @return Commit struct + */ + Commit parseCommit(GVariant* variant, const std::string& branch, const std::string& hash); /** - * @brief OSTreeRepo functions as a C++ wrapper around libostree's OstreeRepo. - * The complete OSTree repository gets parsed into a complete commit list in - * commitList and a list of refs in branches. + * @brief Parse all commits in a OstreeRepo into a commit vector. + * + * @param repo pointer to libostree Ostree repository + * @param checksum checksum of first commit + * @param error gets set, if an error occurred during parsing + * @param commitList commit list to parse the commits into + * @param branch branch to read the commit from + * @param isRecurse !Do not use!, or set to false. Used only for recursion. + * @return true if parsing was successful + * @return false if an error occurred during parsing */ - class OSTreeRepo { - private: - std::string repoPath; - CommitList commitList; - std::vector branches; - - public: - /** - * @brief Construct a new OSTreeRepo. - * - * @param repoPath Path to the OSTree Repository - */ - explicit OSTreeRepo(std::string repoPath); - - /** - * @brief Return a C-style pointer to a libostree OstreeRepo. This exists, to be - * able to access functions, that have not yet been adapted in this C++ wrapper. - * - * @return OstreeRepo* - */ - OstreeRepo* _c(); - - /// Getter - std::string getRepoPath(); - /// Getter - CommitList getCommitList(); - /// Getter - std::vector getBranches(); - - // Methods - - /** - * @brief Reload the OSTree repository data. - * - * @return true if data was changed during the reload - * @return false if nothing changed - */ - bool updateData(); - - /** - * @brief Check if a certain commit is signed. This simply accesses the - * size() of commit.signatures. - * - * @param commit - * @return true if the commit is signed - * @return false if the commit is not signed - */ - static bool isCommitSigned(const Commit& commit); - - /** - * @brief Parse commits from a ostree log output to a commitList, mapping - * the hashes to commits. - * - * @param branch - * @return std::unordered_map - */ - CommitList parseCommitsOfBranch(const std::string& branch); - - /** - * @brief Performs parseCommitsOfBranch() on all available branches and - * merges all commit lists into one. - * - * @return std::unordered_map - */ - CommitList parseCommitsAllBranches(); - - // read & write access to OSTree repo: - - /** - * @brief Promotes a commit to another branch. Similar to: - * `ostree commit --repo=repo -b newRef -s newSubject --tree=ref=hash` - * - * @param hash hash of the commit to promote - * @param newRef branch to promote to - * @param newSubject new commit subject, it needed - * @param keepMetadata should new commit keep metadata of old commit - * @return true on success - * @return false on failed promotion - */ - bool promoteCommit(const std::string& hash, const std::string& newRef, const std::vector addMetadataStrings, const std::string& newSubject = "", bool keepMetadata = true); - - private: - /** - * @brief Get all branches as a single string, separated by spaces. - * - * @return std::string All branch names, separated by spaces - */ - std::string getBranchesAsString(); - - /** - * @brief Parse a libostree GVariant commit to a C++ commit struct. - * - * @param variant pointer to GVariant commit - * @param branch branch of the commit - * @param hash commit hash - * @return Commit struct - */ - Commit parseCommit(GVariant *variant, const std::string& branch, const std::string& hash); - - /** - * @brief Parse all commits in a OstreeRepo into a commit vector. - * - * @param repo pointer to libostree Ostree repository - * @param checksum checksum of first commit - * @param error gets set, if an error occurred during parsing - * @param commitList commit list to parse the commits into - * @param branch branch to read the commit from - * @param isRecurse !Do not use!, or set to false. Used only for recursion. - * @return true if parsing was successful - * @return false if an error occurred during parsing - */ - gboolean parseCommitsRecursive (OstreeRepo *repo, const gchar *checksum, GError **error, - CommitList *commitList, const std::string& branch, - gboolean isRecurse = false); - }; - -} // namespace cpplibostree + gboolean parseCommitsRecursive(OstreeRepo* repo, + const gchar* checksum, + GError** error, + CommitList* commitList, + const std::string& branch, + gboolean isRecurse = false); +}; + +} // namespace cpplibostree