// Include files #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for coloring /*}}}*/ using namespace APT::History; // ============================================================================= // Internal API extensions // ============================================================================= // static Kind ReflectKind(const Kind &kind) { switch (kind) { case Kind::Install: case Kind::Reinstall: return Kind::Remove; case Kind::Upgrade: return Kind::Downgrade; case Kind::Downgrade: return Kind::Upgrade; case Kind::Remove: case Kind::Purge: return Kind::Install; default: assert(false); return kind; } } std::vector APT::Internal::FlattenChanges(const Entry &entry) { std::vector flattened; size_t total_size = 0; for (const auto &[_, changes] : entry.changeMap) total_size += changes.size(); flattened.reserve(total_size); for (const auto &[_, changes] : entry.changeMap) flattened.insert(flattened.end(), changes.begin(), changes.end()); return flattened; } Change APT::Internal::InvertChange(const Change &change) { Change inverse = change; inverse.kind = ReflectKind(change.kind); // tailor change after what action was performed switch (change.kind) { case Kind::Upgrade: std::swap(inverse.currentVersion, inverse.candidateVersion); break; case Kind::Downgrade: std::swap(inverse.currentVersion, inverse.candidateVersion); break; default: break; } return inverse; } // ============================================================================= // Output formatting // ============================================================================= // // ShortenCommand - Take a command and shorten it such that it adheres // to the given maximum length. static std::string ShortenCommand(std::string cmd, const std::size_t maxLen) { if (cmd.starts_with("/usr/bin/")) cmd.erase(0,9); else if (cmd.starts_with("apt ")) cmd.erase(0, 4); if (cmd.length() <= maxLen) return cmd; if (maxLen <= 3) return cmd.substr(0, maxLen); cmd.resize(maxLen - 3); cmd.append("..."); return cmd; } static std::string LocalizeKindToString(const Kind &kind) { switch (kind) { case Kind::Install: return _("Install"); case Kind::Reinstall: return _("Reinstall"); case Kind::Upgrade: return _("Upgrade"); case Kind::Downgrade: return _("Downgrade"); case Kind::Remove: return _("Remove"); case Kind::Purge: return _("Purge"); default: return _("Undefined"); } } // GetKindString - Take a history entry and construct the string // corresponding to the actions performed. Multpile actions are // alphabetically grouped such as: // Install, Remove, and Reinstall -> I,R,rI // // Shorthand legend: // Install -> I // Reinstall -> rI // Upgrade -> U // Downgrade -> D // Remove -> R // Purge -> P // static std::string GetKindString(const Entry &entry) { // We want full output if there is only one action if (entry.changeMap.size() == 1) return LocalizeKindToString(entry.changeMap.begin()->first); std::string kindGroup = ""; // add localization later for (const auto &[action, _] : entry.changeMap) { switch (action) { case Kind::Install: kindGroup += "I"; break; case Kind::Reinstall: kindGroup += "rI"; break; case Kind::Upgrade: kindGroup += "U"; break; case Kind::Downgrade: kindGroup += "D"; break; case Kind::Remove: kindGroup += "R"; break; case Kind::Purge: kindGroup += "P"; break; default: kindGroup += "INVALID"; } kindGroup += ","; } // remove trailing "," if (not kindGroup.empty()) kindGroup.pop_back(); return kindGroup; } // ============================================================================= // Output printing // ============================================================================= // static void PrintHistoryVector(const HistoryBuffer &buf) { // TRANSLATORS: a number used as identifier. const std::string idHeader = _("ID"); // TRANSLATORS: input on the terminal console. const std::string cmdHeader = _("Command line"); // TRANSLATORS: indicates when the entry occurred. const std::string dateHeader = _("Date and Time"); // TRANSLATORS: the type of operation that was performed. const std::string actionHeader = _("Action"); // TRANSLATORS: changes as a quantity. const std::string changesHeader = _("Changes"); constexpr size_t pad = 2; size_t idWidth = std::to_string(buf.size()).size(); idWidth = std::max(idHeader.size(), idWidth) + pad; size_t cmdLineWidth = cmdHeader.size() + pad; size_t dateWidth = dateHeader.size() + pad; size_t actionWidth = actionHeader.size() + pad; size_t changesWidth = changesHeader.size(); // Scale to screen width // ID and Changes are numerical and cannot be shortened size_t remainingWidth = ::ScreenWidth - idWidth - changesWidth; size_t minRequiredCmdWidth = cmdLineWidth; std::vector kindStrings; std::vector changeCounts; kindStrings.reserve(buf.size()); changeCounts.reserve(buf.size()); if (!buf.empty()) { dateWidth = std::max(buf.front().startDate.size() + pad, dateWidth); for (const auto &entry : buf) { std::string kindString = GetKindString(entry); if (kindString.size() > actionWidth) actionWidth = kindString.size(); kindStrings.push_back(kindString); size_t numChanges = 0; for (const auto &[_, changes] : entry.changeMap) numChanges += changes.size(); changeCounts.push_back(numChanges); if (entry.cmdLine.length() > minRequiredCmdWidth) minRequiredCmdWidth = entry.cmdLine.length() + pad; } remainingWidth -= (dateWidth + actionWidth); cmdLineWidth = std::min(remainingWidth, minRequiredCmdWidth); } constexpr auto writeEntry = [](const auto &entry, size_t width) { std::cout << std::left << std::setw(width) << entry; }; writeEntry(idHeader, idWidth); writeEntry(cmdHeader, cmdLineWidth); writeEntry(dateHeader, dateWidth); writeEntry(actionHeader, actionWidth); writeEntry(changesHeader, changesWidth); std::cout << "\n\n"; // Each entry corresponds to a row for (size_t id = 0; id < buf.size(); ++id) { const auto &entry = buf[id]; writeEntry(id, idWidth); writeEntry(ShortenCommand(entry.cmdLine, cmdLineWidth), cmdLineWidth); writeEntry(entry.startDate, dateWidth); // If there are multiple actions, we want to group them writeEntry(kindStrings[id], actionWidth); writeEntry(changeCounts[id], changesWidth); std::cout << "\n"; } } // PrintChange - Take a change and print the event for that package. // Example: // "package (0.1.0, 0.2.0)" and "Upgrade" -> "Upgrade package (0.1.0 -> 0.2.0)" // "package (0.1.0)" and "Install" -> "Install package (0.1.0)" static void PrintChange(const Change &change) { std::cout << " " << LocalizeKindToString(change.kind) << " " << change.package; std::cout << " (" << change.currentVersion; if (not change.candidateVersion.empty()) std::cout << " -> " << change.candidateVersion; if (change.automatic) std::cout << ", " << _("automatic"); std::cout << ")"; } // PrintDetailedEntry - Take a history buffer and print the detailed // transaction details for the given transaction id. static void PrintDetailedEntry(const HistoryBuffer &buf, const size_t id) { Entry entry = buf[id]; std::cout << _("Transaction ID") << ": " << id << "\n"; std::cout << _("Start time") << ": " << entry.startDate << "\n"; std::cout << _("End time") << ": " << entry.endDate << "\n"; std::cout << _("Requested by") << ": " << entry.requestingUser << "\n"; std::cout << _("Command line") << ": " << entry.cmdLine << "\n"; if (not entry.error.empty()) { std::cout << _config->Find("APT::Color::Red") << _("Error") << ": "; std::cout << entry.error << _config->Find("APT::Color::Neutral") << "\n"; } if (not entry.comment.empty()) std::cout << _("Comment") << ": " << entry.comment << "\n"; // For each performed action, print what it did to each package std::cout << _("Packages changed") << ":" << "\n"; for (const auto &[_, changes] : entry.changeMap) { for (const auto &change : changes) { PrintChange(change); std::cout << "\n"; } std::cout << "\n"; } } // ============================================================================= // Transaction helpers // ============================================================================= // class TransactionController { public: TransactionController(CacheFile &cache) : Cache(cache), Fix(Cache.GetDepCache()), InstallAction(Cache, &Fix, false), RemoveAction(Cache, &Fix), HeldBackPackages() { } bool AppendChange(const Change &change) { auto pkg = Cache->FindPkg(change.package); if (pkg.end()) return _error->Error(_("Could not find package %s"), change.package.data()); if (IsRemoval(change.kind)) { auto ver = pkg.CurrentVer(); if (ver == nullptr) { _error->Warning(_("Tried to remove %s, but it is not installed"), change.package.data()); return true; } RemoveAction(ver); return true; } auto ver = pkg.VersionList(); for (; not ver.end(); ++ver) if (strcmp(ver.VerStr(), change.currentVersion.data()) == 0) break; if (ver.end()) return _error->Error(_("Could not find given version %s of %s"), change.currentVersion.data(), change.package.data()); InstallAction(ver); return true; } bool CommitChanges() { InstallAction.doAutoInstall(); OpTextProgress Progress(*_config); bool const resolver_fail = Fix.Resolve(true, &Progress); if (resolver_fail == false && Cache->BrokenCount() == 0) return false; if (CheckNothingBroken(Cache) == false) return false; return InstallPackages(Cache, HeldBackPackages, false, true); } private: // This must be a reference for lifetime safety CacheFile &Cache; pkgProblemResolver Fix; TryToInstall InstallAction; TryToRemove RemoveAction; APT::PackageVector HeldBackPackages; }; static bool ParseId(const char *str, size_t &id, size_t max) { if (str == nullptr) return _error->Error(_("Incorrect usage, no ID was given.")); try { id = std::stoi(str); } catch (const std::invalid_argument &e) { return _error->Error(_("'%s' not a valid ID."), str); } if (max < id) return _error->Error(_("Transaction ID '%ld' out of bounds."), id); return true; } // ============================================================================= // Entrypoints // ============================================================================= // bool DoHistoryUndo(CommandLine &Cmd) { HistoryBuffer buf = {}; if (not ParseLogDir(buf)) return _error->Error(_("Could not read %s"), _config->FindFile("Dir::Log::History").data()); size_t id = 0; if (not ParseId(*(Cmd.FileList + 1), id, buf.size() - 1)) return false; CacheFile Cache; if (Cache.BuildCaches(true) == false) return false; TransactionController Controller(Cache); for (const auto &change : APT::Internal::FlattenChanges(buf[id])) if (not Controller.AppendChange(APT::Internal::InvertChange(change))) return false; return Controller.CommitChanges(); } bool DoHistoryRollback(CommandLine &Cmd) { HistoryBuffer buf = {}; if (not ParseLogDir(buf)) return _error->Error(_("Could not read %s"), _config->FindFile("Dir::Log::History").data()); size_t rollbackId = 0; if (not ParseId(*(Cmd.FileList + 1), rollbackId, buf.size())) return false; CacheFile Cache; if (Cache.BuildCaches(true) == false) return false; TransactionController Controller(Cache); // Map to keep the effective change of each package std::unordered_map effectiveChangeMap; // Plus 1 for zero indexing size_t numChanges = buf.size() - (rollbackId + 1); std::span bufSpan(buf.end() - numChanges, numChanges); // Iterate in reverse to process changes LIFO for (auto it = bufSpan.rbegin(); it != bufSpan.rend(); ++it) for (const auto &change : APT::Internal::FlattenChanges(*it)) effectiveChangeMap[change.package] = APT::Internal::InvertChange(change); for (const auto &[_, change] : effectiveChangeMap) { if (not Controller.AppendChange(change)) return false; } return Controller.CommitChanges(); } bool DoHistoryRedo(CommandLine &Cmd) { HistoryBuffer buf = {}; if (not ParseLogDir(buf)) return _error->Error(_("Could not read %s"), _config->FindFile("Dir::Log::History").data()); size_t id = 0; if (not ParseId(*(Cmd.FileList + 1), id, buf.size() - 1)) return false; CacheFile Cache; if (Cache.BuildCaches(true) == false) return false; TransactionController Controller(Cache); for (const auto &change : APT::Internal::FlattenChanges(buf[id])) if (not Controller.AppendChange(change)) return false; return Controller.CommitChanges(); } bool DoHistoryList(CommandLine &Cmd) { HistoryBuffer buf = {}; if (Cmd.FileSize() != 1) return _error->Error("This command does not support any arguments"); if (not ParseLogDir(buf)) return _error->Error(_("Could not read: %s"), _config->FindFile("Dir::Log::History").data()); PrintHistoryVector(buf); return true; } bool DoHistoryInfo(CommandLine &Cmd) { HistoryBuffer buf = {}; if (not ParseLogDir(buf)) return _error->Error(_("Could not read: %s"), _config->FindFile("Dir::Log::History").data()); size_t id = 0; for (size_t i = 1; i < Cmd.FileSize(); i++) { if (not ParseId(*(Cmd.FileList + i), id, buf.size() - 1)) return false; PrintDetailedEntry(buf, id); } return true; }