GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_history.cc
Date: 2026-10-04 02:40:33
Exec Total Coverage
Lines: 0 692 0.0%
Branches: 0 1630 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System
3 */
4
5 #include "swissknife_history.h"
6
7 #include <algorithm>
8 #include <cassert>
9 #include <ctime>
10
11 #include "catalog_rw.h"
12 #include "crypto/hash.h"
13 #include "history_sqlite.h"
14 #include "manifest_fetch.h"
15 #include "network/download.h"
16 #include "network/sink_path.h"
17 #include "repository_tag.h"
18 #include "upload.h"
19
20 using namespace std; // NOLINT
21 using namespace swissknife; // NOLINT
22
23 const std::string CommandTag::kHeadTag = "trunk";
24 const std::string CommandTag::kPreviousHeadTag = "trunk-previous";
25
26 const std::string CommandTag::kHeadTagDescription = "current HEAD";
27 const std::string
28 CommandTag::kPreviousHeadTagDescription = "default undo target";
29
30 ✗ static void InsertCommonParameters(ParameterList *r) {
31 ✗ r->push_back(Parameter::Mandatory('w', "repository directory / url"));
32 ✗ r->push_back(Parameter::Mandatory('t', "temporary scratch directory"));
33 ✗ r->push_back(Parameter::Optional('p', "public key of the repository"));
34 ✗ r->push_back(Parameter::Optional('f', "fully qualified repository name"));
35 ✗ r->push_back(Parameter::Optional('r', "spooler definition string"));
36 ✗ r->push_back(Parameter::Optional('m', "(unsigned) manifest file to edit"));
37 ✗ r->push_back(Parameter::Optional('b', "mounted repository base hash"));
38 ✗ r->push_back(
39 ✗ Parameter::Optional('e', "hash algorithm to use (default SHA1)"));
40 ✗ r->push_back(Parameter::Switch('L', "follow HTTP redirects"));
41 ✗ r->push_back(Parameter::Optional('P', "session_token_file"));
42 ✗ r->push_back(Parameter::Optional('@', "proxy url"));
43 }
44
45 ✗ CommandTag::Environment *CommandTag::InitializeEnvironment(
46 const ArgumentList &args, const bool read_write) {
47 ✗ const string repository_url = MakeCanonicalPath(*args.find('w')->second);
48 ✗ const string tmp_path = MakeCanonicalPath(*args.find('t')->second);
49 ✗ const string spl_definition = (args.find('r') == args.end())
50 ? ""
51 : MakeCanonicalPath(
52 ✗ *args.find('r')->second);
53 ✗ const string manifest_path = (args.find('m') == args.end())
54 ? ""
55 ✗ : MakeCanonicalPath(*args.find('m')->second);
56 ✗ const shash::Algorithms hash_algo = (args.find('e') == args.end())
57 ✗ ? shash::kSha1
58 ✗ : shash::ParseHashAlgorithm(
59 ✗ *args.find('e')->second);
60 ✗ const string pubkey_path = (args.find('p') == args.end())
61 ? ""
62 ✗ : MakeCanonicalPath(*args.find('p')->second);
63 ✗ const shash::Any base_hash = (args.find('b') == args.end())
64 ? shash::Any()
65 ✗ : shash::MkFromHexPtr(
66 ✗ shash::HexPtr(*args.find('b')->second),
67 ✗ shash::kSuffixCatalog);
68 ✗ const string repo_name = (args.find('f') == args.end())
69 ? ""
70 ✗ : *args.find('f')->second;
71
72 ✗ string session_token_file;
73 ✗ if (args.find('P') != args.end()) {
74 ✗ session_token_file = *args.find('P')->second;
75 }
76
77 // Sanity checks
78 ✗ if (hash_algo == shash::kAny) {
79 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to parse hash algorithm to use");
80 ✗ return NULL;
81 }
82
83 ✗ if (read_write && spl_definition.empty()) {
84 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "no upstream storage provided (-r)");
85 ✗ return NULL;
86 }
87
88 ✗ if (read_write && manifest_path.empty()) {
89 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "no (unsigned) manifest provided (-m)");
90 ✗ return NULL;
91 }
92
93 ✗ if (!read_write && pubkey_path.empty()) {
94 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "no public key provided (-p)");
95 ✗ return NULL;
96 }
97
98 ✗ if (!read_write && repo_name.empty()) {
99 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "no repository name provided (-f)");
100 ✗ return NULL;
101 }
102
103 ✗ if (HasPrefix(spl_definition, "gw", false)) {
104 ✗ if (session_token_file.empty()) {
105 ✗ PrintError("Session token file has to be provided "
106 "when upstream type is gw.");
107 ✗ return NULL;
108 }
109 }
110
111 // create new environment
112 // Note: We use this encapsulation because we cannot be sure that the
113 // Command object gets deleted properly. With the Environment object at
114 // hand we have full control and can make heavy and safe use of RAII
115 ✗ std::unique_ptr<Environment> env(new Environment(repository_url, tmp_path));
116 ✗ env->manifest_path.Set(manifest_path);
117 ✗ env->history_path.Set(CreateTempPath(tmp_path + "/history", 0600));
118
119 // initialize the (swissknife global) download manager
120 ✗ const bool follow_redirects = (args.count('L') > 0);
121 ✗ const std::string &proxy = (args.count('@') > 0) ? *args.find('@')->second
122 ✗ : "";
123 ✗ if (!this->InitDownloadManager(follow_redirects, proxy)) {
124 ✗ return NULL;
125 }
126
127 // initialize the (swissknife global) signature manager (if possible)
128 ✗ if (!pubkey_path.empty() && !this->InitSignatureManager(pubkey_path)) {
129 ✗ return NULL;
130 }
131
132 // open the (yet unsigned) manifest file if it is there, otherwise load the
133 // latest manifest from the server
134 ✗ env->manifest.reset(
135 ✗ (FileExists(env->manifest_path.path()))
136 ✗ ? OpenLocalManifest(env->manifest_path.path())
137 ✗ : FetchRemoteManifest(env->repository_url, repo_name, base_hash));
138
139 ✗ if (env->manifest.get() == nullptr) {
140 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to load manifest file");
141 ✗ return NULL;
142 }
143
144 // figure out the hash of the history from the previous revision if needed
145 ✗ if (read_write && env->manifest->history().IsNull() && !base_hash.IsNull()) {
146 ✗ env->previous_manifest.reset(
147 ✗ FetchRemoteManifest(env->repository_url, repo_name, base_hash));
148 ✗ if (env->previous_manifest.get() == nullptr) {
149 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to load previous manifest");
150 ✗ return NULL;
151 }
152
153 ✗ LogCvmfs(kLogCvmfs, kLogDebug,
154 "using history database '%s' from previous "
155 "manifest (%s) as basis",
156 ✗ env->previous_manifest->history().ToString().c_str(),
157 ✗ env->previous_manifest->repository_name().c_str());
158 ✗ env->manifest->set_history(env->previous_manifest->history());
159 ✗ env->manifest->set_repository_name(
160 ✗ env->previous_manifest->repository_name());
161 }
162
163 // download the history database referenced in the manifest
164 ✗ env->history.reset(GetHistory(env->manifest.get(), env->repository_url,
165 ✗ env->history_path.path(), read_write));
166 ✗ if (env->history.get() == nullptr) {
167 ✗ return NULL;
168 }
169
170 // if the using Command is expected to change the history database, we
171 // need
172 // to initialize the upload spooler for potential later history upload
173 ✗ if (read_write) {
174 ✗ const bool use_file_chunking = false;
175 ✗ const bool generate_legacy_bulk_chunks = false;
176 const upload::SpoolerDefinition sd(
177 spl_definition, hash_algo, zlib::kZlibDefault,
178 generate_legacy_bulk_chunks, use_file_chunking, 0, 0, 0,
179 ✗ session_token_file);
180 ✗ env->spooler.reset(upload::Spooler::Construct(sd));
181 ✗ if (env->spooler.get() == nullptr) {
182 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to initialize upload spooler");
183 ✗ return NULL;
184 }
185 }
186
187 // return the pointer of the Environment (passing the ownership along)
188 ✗ return env.release();
189 }
190
191 ✗ bool CommandTag::CloseAndPublishHistory(Environment *env) {
192 ✗ assert(env->spooler.get() != nullptr);
193
194 // set the previous revision pointer of the history database
195 ✗ env->history->SetPreviousRevision(env->manifest->history());
196
197 // close the history database
198 ✗ history::History *weak_history = env->history.release();
199 ✗ delete weak_history;
200
201 // compress and upload the new history database
202 ✗ Future<shash::Any> history_hash;
203 ✗ upload::Spooler::CallbackPtr callback = env->spooler->RegisterListener(
204 &CommandTag::UploadClosure, this, &history_hash);
205 ✗ env->spooler->ProcessHistory(env->history_path.path());
206 ✗ env->spooler->WaitForUpload();
207 ✗ const shash::Any new_history_hash = history_hash.Get();
208 ✗ env->spooler->UnregisterListener(callback);
209
210 // retrieve the (async) uploader result
211 ✗ if (new_history_hash.IsNull()) {
212 ✗ return false;
213 }
214
215 // update the (yet unsigned) manifest file
216 ✗ env->manifest->set_history(new_history_hash);
217 ✗ if (!env->manifest->Export(env->manifest_path.path())) {
218 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to export the new manifest '%s'",
219 ✗ env->manifest_path.path().c_str());
220 ✗ return false;
221 }
222
223 // disable the unlink guard in order to keep the newly exported manifest file
224 ✗ env->manifest_path.Disable();
225 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg,
226 "exported manifest (%" PRIu64 ") with new history '%s'",
227 ✗ env->manifest->revision(), new_history_hash.ToString().c_str());
228
229 ✗ return true;
230 }
231
232
233 ✗ bool CommandTag::UploadCatalogAndUpdateManifest(
234 CommandTag::Environment *env, catalog::WritableCatalog *catalog) {
235 ✗ assert(env->spooler.get() != nullptr);
236
237 // gather information about catalog to be uploaded and update manifest
238 ✗ std::unique_ptr<catalog::WritableCatalog> wr_catalog(catalog);
239 ✗ const std::string catalog_path = wr_catalog->database_path();
240 ✗ env->manifest->set_ttl(wr_catalog->GetTTL());
241 ✗ env->manifest->set_revision(wr_catalog->GetRevision());
242 ✗ env->manifest->set_publish_timestamp(wr_catalog->GetLastModified());
243
244 // close the catalog
245 ✗ catalog::WritableCatalog *weak_catalog = wr_catalog.release();
246 ✗ delete weak_catalog;
247
248 // upload the catalog
249 ✗ Future<shash::Any> catalog_hash;
250 ✗ upload::Spooler::CallbackPtr callback = env->spooler->RegisterListener(
251 &CommandTag::UploadClosure, this, &catalog_hash);
252 ✗ env->spooler->ProcessCatalog(catalog_path);
253 ✗ env->spooler->WaitForUpload();
254 ✗ const shash::Any new_catalog_hash = catalog_hash.Get();
255 ✗ env->spooler->UnregisterListener(callback);
256
257 // check if the upload succeeded
258 ✗ if (new_catalog_hash.IsNull()) {
259 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to upload catalog '%s'",
260 catalog_path.c_str());
261 ✗ return false;
262 }
263
264 // update the catalog size and hash in the manifest
265 ✗ const size_t catalog_size = GetFileSize(catalog_path);
266 ✗ env->manifest->set_catalog_size(catalog_size);
267 ✗ env->manifest->set_catalog_hash(new_catalog_hash);
268
269 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg, "uploaded new catalog (%lu bytes) '%s'",
270 ✗ catalog_size, new_catalog_hash.ToString().c_str());
271
272 ✗ return true;
273 }
274
275 ✗ void CommandTag::UploadClosure(const upload::SpoolerResult &result,
276 Future<shash::Any> *hash) {
277 ✗ assert(!result.IsChunked());
278 ✗ if (result.return_code != 0) {
279 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to upload history database (%d)",
280 ✗ result.return_code);
281 ✗ hash->Set(shash::Any());
282 } else {
283 ✗ hash->Set(result.content_hash);
284 }
285 }
286
287 ✗ bool CommandTag::UpdateUndoTags(
288 Environment *env, const history::History::Tag &current_head_template,
289 const bool undo_rollback) {
290 ✗ assert(env->history.get() != nullptr);
291
292 ✗ history::History::Tag current_head;
293 ✗ history::History::Tag current_old_head;
294
295 // remove previous HEAD tag
296 ✗ if (!env->history->Remove(CommandTag::kPreviousHeadTag)) {
297 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg, "didn't find a previous HEAD tag");
298 }
299
300 // check if we have a current HEAD tag that needs to renamed to previous
301 // HEAD
302 ✗ if (env->history->GetByName(CommandTag::kHeadTag, &current_head)) {
303 // remove current HEAD tag
304 ✗ if (!env->history->Remove(CommandTag::kHeadTag)) {
305 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to remove current HEAD tag");
306 ✗ return false;
307 }
308
309 // set previous HEAD tag where current HEAD used to be
310 ✗ if (!undo_rollback) {
311 ✗ current_old_head = current_head;
312 ✗ current_old_head.name = CommandTag::kPreviousHeadTag;
313 ✗ current_old_head.description = CommandTag::kPreviousHeadTagDescription;
314 ✗ if (!env->history->Insert(current_old_head)) {
315 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to set previous HEAD tag");
316 ✗ return false;
317 }
318 }
319 }
320
321 // set the current HEAD to the catalog provided by the template HEAD
322 ✗ current_head = current_head_template;
323 ✗ current_head.name = CommandTag::kHeadTag;
324 ✗ current_head.description = CommandTag::kHeadTagDescription;
325 ✗ if (!env->history->Insert(current_head)) {
326 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to set new current HEAD");
327 ✗ return false;
328 }
329
330 ✗ return true;
331 }
332
333 ✗ bool CommandTag::FetchObject(const std::string &repository_url,
334 const shash::Any &object_hash,
335 const std::string &destination_path) const {
336 ✗ assert(!object_hash.IsNull());
337
338 download::Failures dl_retval;
339 ✗ const std::string url = repository_url + "/data/" + object_hash.MakePath();
340
341 ✗ cvmfs::PathSink pathsink(destination_path);
342 ✗ download::JobInfo download_object(&url, true, false, &object_hash, &pathsink);
343 ✗ dl_retval = download_manager()->Fetch(&download_object);
344
345 ✗ if (dl_retval != download::kFailOk) {
346 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to download object '%s' (%d - %s)",
347 ✗ object_hash.ToStringWithSuffix().c_str(), dl_retval,
348 download::Code2Ascii(dl_retval));
349 ✗ return false;
350 }
351
352 ✗ return true;
353 }
354
355 ✗ history::History *CommandTag::GetHistory(const manifest::Manifest *manifest,
356 const std::string &repository_url,
357 const std::string &history_path,
358 const bool read_write) const {
359 ✗ const shash::Any history_hash = manifest->history();
360 history::History *history;
361
362 ✗ if (history_hash.IsNull()) {
363 ✗ history = history::SqliteHistory::Create(history_path,
364 ✗ manifest->repository_name());
365 ✗ if (NULL == history) {
366 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to create history database");
367 ✗ return NULL;
368 }
369 } else {
370 ✗ if (!FetchObject(repository_url, history_hash, history_path)) {
371 ✗ return NULL;
372 }
373
374 ✗ history = (read_write) ? history::SqliteHistory::OpenWritable(history_path)
375 ✗ : history::SqliteHistory::Open(history_path);
376 ✗ if (NULL == history) {
377 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to open history database (%s)",
378 history_path.c_str());
379 ✗ unlink(history_path.c_str());
380 ✗ return NULL;
381 }
382
383 ✗ assert(history->fqrn() == manifest->repository_name());
384 }
385
386 ✗ return history;
387 }
388
389 ✗ catalog::Catalog *CommandTag::GetCatalog(const std::string &repository_url,
390 const shash::Any &catalog_hash,
391 const std::string catalog_path,
392 const bool read_write) const {
393 ✗ assert(shash::kSuffixCatalog == catalog_hash.suffix);
394 ✗ if (!FetchObject(repository_url, catalog_hash, catalog_path)) {
395 ✗ return NULL;
396 }
397
398 ✗ const std::string catalog_root_path = "";
399 ✗ return (read_write) ? catalog::WritableCatalog::AttachFreely(
400 catalog_root_path, catalog_path, catalog_hash)
401 ✗ : catalog::Catalog::AttachFreely(
402 ✗ catalog_root_path, catalog_path, catalog_hash);
403 }
404
405 ✗ void CommandTag::PrintTagMachineReadable(
406 const history::History::Tag &tag) const {
407 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "%s %s %" PRIu64 " %" PRIu64 " %ld %s %s",
408 ✗ tag.name.c_str(), tag.root_hash.ToString().c_str(), tag.size,
409 ✗ tag.revision, tag.timestamp,
410 ✗ (tag.branch == "") ? "(default)" : tag.branch.c_str(),
411 tag.description.c_str());
412 }
413
414 ✗ std::string CommandTag::AddPadding(const std::string &str, const size_t padding,
415 const bool align_right,
416 const std::string &fill_char) const {
417 ✗ assert(str.size() <= padding);
418 ✗ std::string result(str);
419 ✗ result.resize(padding);
420 ✗ const size_t pos = (align_right) ? 0 : str.size();
421 ✗ const size_t padding_width = padding - str.size();
422 ✗ for (size_t i = 0; i < padding_width; ++i)
423 ✗ result.insert(pos, fill_char);
424 ✗ return result;
425 }
426
427 ✗ bool CommandTag::IsUndoTagName(const std::string &tag_name) const {
428 ✗ return tag_name == CommandTag::kHeadTag
429 ✗ || tag_name == CommandTag::kPreviousHeadTag;
430 }
431
432 //------------------------------------------------------------------------------
433
434 ✗ ParameterList CommandEditTag::GetParams() const {
435 ✗ ParameterList r;
436 ✗ InsertCommonParameters(&r);
437
438 ✗ r.push_back(Parameter::Optional('d', "space separated tags to be deleted"));
439 ✗ r.push_back(Parameter::Optional('a', "name of the new tag"));
440 ✗ r.push_back(Parameter::Optional('D', "description of the tag"));
441 ✗ r.push_back(Parameter::Optional('B', "branch of the new tag"));
442 ✗ r.push_back(Parameter::Optional('P', "predecessor branch"));
443 ✗ r.push_back(Parameter::Optional('h', "root hash of the new tag"));
444 ✗ r.push_back(Parameter::Switch('x', "maintain undo tags"));
445 ✗ r.push_back(Parameter::Optional(
446 'c', "cleanup auto tags older than this Unix timestamp"));
447 ✗ return r;
448 }
449
450 ✗ int CommandEditTag::Main(const ArgumentList &args) {
451 ✗ if ((args.find('d') == args.end()) && (args.find('a') == args.end())
452 ✗ && (args.find('x') == args.end()) && (args.find('c') == args.end())) {
453 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "nothing to do");
454 ✗ return 1;
455 }
456
457 // initialize the Environment (taking ownership)
458 ✗ const bool history_read_write = true;
459 const std::unique_ptr<Environment> env(
460 ✗ InitializeEnvironment(args, history_read_write));
461 ✗ if (env.get() == nullptr) {
462 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
463 ✗ return 1;
464 }
465
466 int retval;
467 ✗ if (args.find('d') != args.end()) {
468 ✗ retval = RemoveTags(args, env.get());
469 ✗ if (retval != 0)
470 ✗ return retval;
471 }
472
473 // Cleanup old auto-generated tags if -c is specified. This must run *before*
474 // adding the new tag below, so that the freshly created auto tag is never
475 // itself a cleanup candidate: with a cutoff at (or in) the future (e.g.
476 // CVMFS_AUTO_TAG_TIMESPAN="tomorrow") the new tag would otherwise be born
477 // older than the threshold and immediately removed. This matches the
478 // non-gateway publish ordering (cleanup before tagging), so the latest auto
479 // tag always survives.
480 ✗ if (args.find('c') != args.end()) {
481 ✗ retval = CleanupOldAutoTags(args, env.get());
482 ✗ if (retval != 0)
483 ✗ return retval;
484 }
485
486 ✗ if ((args.find('a') != args.end()) || (args.find('x') != args.end())) {
487 ✗ retval = AddNewTag(args, env.get());
488 ✗ if (retval != 0)
489 ✗ return retval;
490 }
491
492 // finalize processing and upload new history database
493 ✗ if (!CloseAndPublishHistory(env.get())) {
494 ✗ return 1;
495 }
496 ✗ return 0;
497 }
498
499 ✗ int CommandEditTag::CleanupOldAutoTags(const ArgumentList &args,
500 Environment *env) {
501 ✗ const time_t threshold = String2Int64(*args.find('c')->second);
502 ✗ if (threshold <= 0) {
503 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid cleanup threshold timestamp");
504 ✗ return 1;
505 }
506
507 // List all tags
508 typedef std::vector<history::History::Tag> TagList;
509 ✗ TagList tags;
510 ✗ if (!env->history->List(&tags)) {
511 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to list tags for auto cleanup");
512 ✗ return 1;
513 }
514
515 // Filter for auto-generated tags that are older than the threshold
516 ✗ std::vector<std::string> condemned_tags;
517 ✗ for (TagList::const_iterator i = tags.begin(); i != tags.end(); ++i) {
518 ✗ if (RepositoryTag::IsAutoGeneratedName(i->name)
519 ✗ && i->timestamp < threshold) {
520 ✗ condemned_tags.push_back(i->name);
521 }
522 }
523
524 ✗ if (condemned_tags.empty()) {
525 ✗ LogCvmfs(kLogCvmfs, kLogDebug, "no outdated auto tags to clean up");
526 ✗ return 0;
527 }
528
529 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "cleaning up %lu outdated auto tags",
530 condemned_tags.size());
531
532 // Delete the old auto tags
533 ✗ env->history->BeginTransaction();
534 ✗ for (std::vector<std::string>::const_iterator i = condemned_tags.begin();
535 ✗ i != condemned_tags.end();
536 ✗ ++i) {
537 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "removing auto tag '%s'", i->c_str());
538 ✗ if (!env->history->Remove(*i)) {
539 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
540 "failed to remove auto tag '%s' from history", i->c_str());
541 ✗ return 1;
542 }
543 }
544 ✗ const bool retval = env->history->PruneBranches();
545 ✗ if (!retval) {
546 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
547 "failed to prune unused branches from history");
548 ✗ return 1;
549 }
550 ✗ env->history->CommitTransaction();
551 ✗ if (!env->history->Vacuum()) {
552 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to vacuum history after cleanup");
553 ✗ return 1;
554 }
555
556 ✗ return 0;
557 }
558
559 ✗ int CommandEditTag::AddNewTag(const ArgumentList &args, Environment *env) {
560 ✗ const std::string tag_name = (args.find('a') != args.end())
561 ✗ ? *args.find('a')->second
562 ✗ : "";
563 ✗ const std::string tag_description = (args.find('D') != args.end())
564 ✗ ? *args.find('D')->second
565 ✗ : "";
566 ✗ const bool undo_tags = (args.find('x') != args.end());
567 ✗ const std::string root_hash_string = (args.find('h') != args.end())
568 ✗ ? *args.find('h')->second
569 ✗ : "";
570 ✗ const std::string branch_name = (args.find('B') != args.end())
571 ✗ ? *args.find('B')->second
572 ✗ : "";
573 ✗ const std::string previous_branch_name = (args.find('P') != args.end())
574 ✗ ? *args.find('P')->second
575 ✗ : "";
576
577 ✗ if (tag_name.find(" ") != std::string::npos) {
578 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "tag names must not contain spaces");
579 ✗ return 1;
580 }
581
582 ✗ assert(!tag_name.empty() || undo_tags);
583
584 ✗ if (IsUndoTagName(tag_name)) {
585 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "undo tags are managed internally");
586 ✗ return 1;
587 }
588
589 // set the root hash to be tagged to the current HEAD if no other hash was
590 // given by the user
591 ✗ const shash::Any root_hash = GetTagRootHash(env, root_hash_string);
592 ✗ if (root_hash.IsNull()) {
593 ✗ return 1;
594 }
595
596 // open the catalog to be tagged (to check for existence and for meta info)
597 const UnlinkGuard catalog_path(
598 ✗ CreateTempPath(env->tmp_path + "/catalog", 0600));
599 ✗ const bool catalog_read_write = false;
600 const std::unique_ptr<catalog::Catalog> catalog(GetCatalog(
601 ✗ env->repository_url, root_hash, catalog_path.path(), catalog_read_write));
602 ✗ if (catalog.get() == nullptr) {
603 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "catalog with hash '%s' does not exist",
604 ✗ root_hash.ToString().c_str());
605 ✗ return 1;
606 }
607
608 // check if the catalog is a root catalog
609 ✗ if (!catalog->root_prefix().IsEmpty()) {
610 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
611 "cannot tag catalog '%s' that is not a "
612 "root catalog.",
613 ✗ root_hash.ToString().c_str());
614 ✗ return 1;
615 }
616
617 // create a template for the new tag to be created, moved or used as undo tag
618 ✗ history::History::Tag tag_template;
619 ✗ tag_template.name = "<template>";
620 ✗ tag_template.root_hash = root_hash;
621 ✗ tag_template.size = GetFileSize(catalog_path.path());
622 ✗ tag_template.revision = catalog->GetRevision();
623 ✗ tag_template.timestamp = catalog->GetLastModified();
624 ✗ tag_template.branch = branch_name;
625 ✗ tag_template.description = tag_description;
626
627 // manipulate the tag database by creating a new tag or moving an existing one
628 ✗ if (!tag_name.empty()) {
629 ✗ tag_template.name = tag_name;
630 ✗ const bool user_provided_hash = (!root_hash_string.empty());
631
632 ✗ if (!env->history->ExistsBranch(tag_template.branch)) {
633 const history::History::Branch branch(
634 ✗ tag_template.branch, previous_branch_name, tag_template.revision);
635 ✗ if (!env->history->InsertBranch(branch)) {
636 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "cannot insert branch '%s'",
637 tag_template.branch.c_str());
638 ✗ return 1;
639 }
640 }
641
642 ✗ if (!ManipulateTag(env, tag_template, user_provided_hash)) {
643 ✗ return 1;
644 }
645 }
646
647 // handle undo tags ('trunk' and 'trunk-previous') if necessary
648 ✗ if (undo_tags && !UpdateUndoTags(env, tag_template)) {
649 ✗ return 1;
650 }
651
652 ✗ return 0;
653 }
654
655 ✗ shash::Any CommandEditTag::GetTagRootHash(
656 Environment *env, const std::string &root_hash_string) const {
657 ✗ shash::Any root_hash;
658
659 ✗ if (root_hash_string.empty()) {
660 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg,
661 "no catalog hash provided, using hash"
662 "of current HEAD catalog (%s)",
663 ✗ env->manifest->catalog_hash().ToString().c_str());
664 ✗ root_hash = env->manifest->catalog_hash();
665 } else {
666 ✗ root_hash = shash::MkFromHexPtr(shash::HexPtr(root_hash_string),
667 shash::kSuffixCatalog);
668 ✗ if (root_hash.IsNull()) {
669 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
670 "failed to read provided catalog hash '%s'",
671 root_hash_string.c_str());
672 }
673 }
674
675 ✗ return root_hash;
676 }
677
678 ✗ bool CommandEditTag::ManipulateTag(Environment *env,
679 const history::History::Tag &tag_template,
680 const bool user_provided_hash) {
681 ✗ const std::string &tag_name = tag_template.name;
682
683 // check if the tag already exists, otherwise create it and return
684 ✗ if (!env->history->Exists(tag_name)) {
685 ✗ return CreateTag(env, tag_template);
686 }
687
688 // tag does exist already, now we need to see if we can move it
689 ✗ if (!user_provided_hash) {
690 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
691 "a tag with the name '%s' already exists. Do you want to move it? "
692 "(-h <root hash>)",
693 tag_name.c_str());
694 ✗ return false;
695 }
696
697 // move the already existing tag and return
698 ✗ return MoveTag(env, tag_template);
699 }
700
701 ✗ bool CommandEditTag::MoveTag(Environment *env,
702 const history::History::Tag &tag_template) {
703 ✗ const std::string &tag_name = tag_template.name;
704 ✗ history::History::Tag new_tag = tag_template;
705
706 // get the already existent tag
707 ✗ history::History::Tag old_tag;
708 ✗ if (!env->history->GetByName(tag_name, &old_tag)) {
709 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to retrieve tag '%s' for moving",
710 tag_name.c_str());
711 ✗ return false;
712 }
713
714 // check if we would move the tag to the same hash
715 ✗ if (old_tag.root_hash == new_tag.root_hash) {
716 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' already points to '%s'",
717 ✗ tag_name.c_str(), old_tag.root_hash.ToString().c_str());
718 ✗ return false;
719 }
720
721 // copy over old description if no new description was given
722 ✗ if (new_tag.description.empty()) {
723 ✗ new_tag.description = old_tag.description;
724 }
725 ✗ new_tag.branch = old_tag.branch;
726
727 // remove the old tag from the database
728 ✗ if (!env->history->Remove(tag_name)) {
729 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "removing old tag '%s' before move failed",
730 tag_name.c_str());
731 ✗ return false;
732 }
733 ✗ if (!env->history->PruneBranches()) {
734 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "could not prune unused branches");
735 ✗ return false;
736 }
737 ✗ const bool retval = env->history->Vacuum();
738 ✗ assert(retval);
739
740 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "moving tag '%s' from '%s' to '%s'",
741 ✗ tag_name.c_str(), old_tag.root_hash.ToString().c_str(),
742 ✗ tag_template.root_hash.ToString().c_str());
743
744 // re-create the moved tag
745 ✗ return CreateTag(env, new_tag);
746 }
747
748 ✗ bool CommandEditTag::CreateTag(Environment *env,
749 const history::History::Tag &new_tag) {
750 ✗ if (!env->history->Insert(new_tag)) {
751 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to insert new tag '%s'",
752 new_tag.name.c_str());
753 ✗ return false;
754 }
755
756 ✗ return true;
757 }
758
759 ✗ int CommandEditTag::RemoveTags(const ArgumentList &args, Environment *env) {
760 typedef std::vector<std::string> TagNames;
761 ✗ const std::string tags_to_delete = *args.find('d')->second;
762
763 ✗ const TagNames condemned_tags = SplitString(tags_to_delete, ' ');
764
765 // check if user tries to remove a magic undo tag
766 ✗ TagNames::const_iterator i = condemned_tags.begin();
767 ✗ const TagNames::const_iterator iend = condemned_tags.end();
768 ✗ for (; i != iend; ++i) {
769 ✗ if (IsUndoTagName(*i)) {
770 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
771 "undo tags are handled internally and cannot be deleted");
772 ✗ return 1;
773 }
774 }
775
776 ✗ LogCvmfs(kLogCvmfs, kLogDebug, "proceeding to delete %lu tags",
777 condemned_tags.size());
778
779 // check if the tags to be deleted exist
780 ✗ bool all_exist = true;
781 ✗ for (i = condemned_tags.begin(); i != iend; ++i) {
782 ✗ if (!env->history->Exists(*i)) {
783 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist", i->c_str());
784 ✗ all_exist = false;
785 }
786 }
787 ✗ if (!all_exist) {
788 ✗ return 1;
789 }
790
791 // delete the tags from the tag database and print their root hashes
792 ✗ i = condemned_tags.begin();
793 ✗ env->history->BeginTransaction();
794 ✗ for (; i != iend; ++i) {
795 // print some information about the tag to be deleted
796 ✗ history::History::Tag condemned_tag;
797 ✗ const bool found_tag = env->history->GetByName(*i, &condemned_tag);
798 ✗ assert(found_tag);
799 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "deleting '%s' (%s)",
800 condemned_tag.name.c_str(),
801 ✗ condemned_tag.root_hash.ToString().c_str());
802
803 // remove the tag
804 ✗ if (!env->history->Remove(*i)) {
805 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to remove tag '%s' from history",
806 i->c_str());
807 ✗ return 1;
808 }
809 }
810 ✗ bool retval = env->history->PruneBranches();
811 ✗ if (!retval) {
812 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
813 "failed to prune unused branches from history");
814 ✗ return 1;
815 }
816 ✗ env->history->CommitTransaction();
817 ✗ retval = env->history->Vacuum();
818 ✗ assert(retval);
819
820 ✗ return 0;
821 }
822
823 //------------------------------------------------------------------------------
824
825
826 ✗ ParameterList CommandListTags::GetParams() const {
827 ✗ ParameterList r;
828 ✗ InsertCommonParameters(&r);
829 ✗ r.push_back(Parameter::Switch('x', "machine readable output"));
830 ✗ r.push_back(Parameter::Switch('B', "print branch hierarchy"));
831 ✗ return r;
832 }
833
834 ✗ void CommandListTags::PrintHumanReadableTagList(
835 const CommandListTags::TagList &tags) const {
836 // go through the list of tags and figure out the column widths
837 ✗ const std::string name_label = "Name";
838 ✗ const std::string rev_label = "Revision";
839 ✗ const std::string time_label = "Timestamp";
840 ✗ const std::string branch_label = "Branch";
841 ✗ const std::string desc_label = "Description";
842
843 // figure out the maximal lengths of the fields in the lists
844 ✗ TagList::const_reverse_iterator i = tags.rbegin();
845 ✗ const TagList::const_reverse_iterator iend = tags.rend();
846 ✗ size_t max_name_len = name_label.size();
847 ✗ size_t max_rev_len = rev_label.size();
848 ✗ size_t max_time_len = desc_label.size();
849 ✗ size_t max_branch_len = branch_label.size();
850 ✗ for (; i != iend; ++i) {
851 ✗ max_name_len = std::max(max_name_len, i->name.size());
852 ✗ max_rev_len = std::max(max_rev_len, StringifyInt(i->revision).size());
853 ✗ max_time_len = std::max(max_time_len,
854 ✗ StringifyTime(i->timestamp, true).size());
855 ✗ max_branch_len = std::max(max_branch_len, i->branch.size());
856 }
857
858 // print the list header
859 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "%s \u2502 %s \u2502 %s \u2502 %s \u2502 %s",
860 ✗ AddPadding(name_label, max_name_len).c_str(),
861 ✗ AddPadding(rev_label, max_rev_len).c_str(),
862 ✗ AddPadding(time_label, max_time_len).c_str(),
863 ✗ AddPadding(branch_label, max_branch_len).c_str(),
864 desc_label.c_str());
865 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
866 "%s\u2500\u253C\u2500%s\u2500\u253C\u2500%s"
867 "\u2500\u253C\u2500%s\u2500\u253C\u2500%s",
868 ✗ AddPadding("", max_name_len, false, "\u2500").c_str(),
869 ✗ AddPadding("", max_rev_len, false, "\u2500").c_str(),
870 ✗ AddPadding("", max_time_len, false, "\u2500").c_str(),
871 ✗ AddPadding("", max_branch_len, false, "\u2500").c_str(),
872 ✗ AddPadding("", desc_label.size() + 1, false, "\u2500").c_str());
873
874 // print the rows of the list
875 ✗ i = tags.rbegin();
876 ✗ for (; i != iend; ++i) {
877 ✗ LogCvmfs(
878 kLogCvmfs, kLogStdout, "%s \u2502 %s \u2502 %s \u2502 %s \u2502 %s",
879 ✗ AddPadding(i->name, max_name_len).c_str(),
880 ✗ AddPadding(StringifyInt(i->revision), max_rev_len, true).c_str(),
881 ✗ AddPadding(StringifyTime(i->timestamp, true), max_time_len).c_str(),
882 ✗ AddPadding(i->branch, max_branch_len).c_str(), i->description.c_str());
883 }
884
885 // print the list footer
886 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
887 "%s\u2500\u2534\u2500%s\u2500\u2534\u2500%s"
888 "\u2500\u2534\u2500%s\u2500\u2534\u2500%s",
889 ✗ AddPadding("", max_name_len, false, "\u2500").c_str(),
890 ✗ AddPadding("", max_rev_len, false, "\u2500").c_str(),
891 ✗ AddPadding("", max_time_len, false, "\u2500").c_str(),
892 ✗ AddPadding("", max_branch_len, false, "\u2500").c_str(),
893 ✗ AddPadding("", desc_label.size() + 1, false, "\u2500").c_str());
894
895 // print the number of tags listed
896 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "listing contains %lu tags", tags.size());
897 }
898
899 ✗ void CommandListTags::PrintMachineReadableTagList(const TagList &tags) const {
900 ✗ TagList::const_iterator i = tags.begin();
901 ✗ const TagList::const_iterator iend = tags.end();
902 ✗ for (; i != iend; ++i) {
903 ✗ PrintTagMachineReadable(*i);
904 }
905 }
906
907
908 ✗ void CommandListTags::PrintHumanReadableBranchList(
909 const BranchHierarchy &branches) const {
910 ✗ const unsigned N = branches.size();
911 ✗ for (unsigned i = 0; i < N; ++i) {
912 ✗ for (unsigned l = 0; l < branches[i].level; ++l) {
913 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, "%s",
914 ✗ ((l + 1) == branches[i].level) ? "\u251c " : "\u2502 ");
915 }
916 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "%s @%" PRIu64,
917 ✗ branches[i].branch.branch.c_str(),
918 ✗ branches[i].branch.initial_revision);
919 }
920 }
921
922
923 ✗ void CommandListTags::PrintMachineReadableBranchList(
924 const BranchHierarchy &branches) const {
925 ✗ const unsigned N = branches.size();
926 ✗ for (unsigned i = 0; i < N; ++i) {
927 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "[%u] %s%s @%" PRIu64, branches[i].level,
928 ✗ AddPadding("", branches[i].level, false, " ").c_str(),
929 ✗ branches[i].branch.branch.c_str(),
930 ✗ branches[i].branch.initial_revision);
931 }
932 }
933
934
935 ✗ void CommandListTags::SortBranchesRecursively(
936 unsigned level,
937 const string &parent_branch,
938 const BranchList &branches,
939 BranchHierarchy *hierarchy) const {
940 // For large numbers of branches, this should be turned into the O(n) version
941 // using a linked list
942 ✗ const unsigned N = branches.size();
943 ✗ for (unsigned i = 0; i < N; ++i) {
944 ✗ if (branches[i].branch == "")
945 ✗ continue;
946 ✗ if (branches[i].parent == parent_branch) {
947 ✗ hierarchy->push_back(BranchLevel(branches[i], level));
948 ✗ SortBranchesRecursively(level + 1, branches[i].branch, branches,
949 hierarchy);
950 }
951 }
952 }
953
954
955 ✗ CommandListTags::BranchHierarchy CommandListTags::SortBranches(
956 const BranchList &branches) const {
957 ✗ BranchHierarchy hierarchy;
958 ✗ hierarchy.push_back(
959 ✗ BranchLevel(history::History::Branch("(default)", "", 0), 0));
960 ✗ SortBranchesRecursively(1, "", branches, &hierarchy);
961 ✗ return hierarchy;
962 }
963
964
965 ✗ int CommandListTags::Main(const ArgumentList &args) {
966 ✗ const bool machine_readable = (args.find('x') != args.end());
967 ✗ const bool branch_hierarchy = (args.find('B') != args.end());
968
969 // initialize the Environment (taking ownership)
970 ✗ const bool history_read_write = false;
971 const std::unique_ptr<Environment> env(
972 ✗ InitializeEnvironment(args, history_read_write));
973 ✗ if (env.get() == nullptr) {
974 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
975 ✗ return 1;
976 }
977
978 ✗ if (branch_hierarchy) {
979 ✗ BranchList branch_list;
980 ✗ if (!env->history->ListBranches(&branch_list)) {
981 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
982 "failed to list branches in history database");
983 ✗ return 1;
984 }
985 ✗ const BranchHierarchy branch_hierarchy = SortBranches(branch_list);
986
987 ✗ if (machine_readable) {
988 ✗ PrintMachineReadableBranchList(branch_hierarchy);
989 } else {
990 ✗ PrintHumanReadableBranchList(branch_hierarchy);
991 }
992 ✗ } else {
993 // obtain a full list of all tags
994 ✗ TagList tags;
995 ✗ if (!env->history->List(&tags)) {
996 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
997 "failed to list tags in history database");
998 ✗ return 1;
999 }
1000
1001 ✗ if (machine_readable) {
1002 ✗ PrintMachineReadableTagList(tags);
1003 } else {
1004 ✗ PrintHumanReadableTagList(tags);
1005 }
1006 }
1007
1008 ✗ return 0;
1009 }
1010
1011 //------------------------------------------------------------------------------
1012
1013 ✗ ParameterList CommandInfoTag::GetParams() const {
1014 ✗ ParameterList r;
1015 ✗ InsertCommonParameters(&r);
1016
1017 ✗ r.push_back(Parameter::Mandatory('n', "name of the tag to be inspected"));
1018 ✗ r.push_back(Parameter::Switch('x', "machine readable output"));
1019 ✗ return r;
1020 }
1021
1022 ✗ std::string CommandInfoTag::HumanReadableFilesize(const size_t filesize) const {
1023 ✗ const size_t kiB = 1024;
1024 ✗ const size_t MiB = kiB * 1024;
1025 ✗ const size_t GiB = MiB * 1024;
1026
1027 ✗ if (filesize > GiB) {
1028 ✗ return StringifyDouble(static_cast<double>(filesize) / GiB) + " GiB";
1029 ✗ } else if (filesize > MiB) {
1030 ✗ return StringifyDouble(static_cast<double>(filesize) / MiB) + " MiB";
1031 ✗ } else if (filesize > kiB) {
1032 ✗ return StringifyDouble(static_cast<double>(filesize) / kiB) + " kiB";
1033 } else {
1034 ✗ return StringifyInt(filesize) + " Byte";
1035 }
1036 }
1037
1038 ✗ void CommandInfoTag::PrintHumanReadableInfo(
1039 const history::History::Tag &tag) const {
1040 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
1041 "Name: %s\n"
1042 "Revision: %" PRIu64 "\n"
1043 "Timestamp: %s\n"
1044 "Branch: %s\n"
1045 "Root Hash: %s\n"
1046 "Catalog Size: %s\n"
1047 "%s",
1048 ✗ tag.name.c_str(), tag.revision,
1049 ✗ StringifyTime(tag.timestamp, true /* utc */).c_str(),
1050 ✗ tag.branch.c_str(), tag.root_hash.ToString().c_str(),
1051 ✗ HumanReadableFilesize(tag.size).c_str(), tag.description.c_str());
1052 }
1053
1054 ✗ int CommandInfoTag::Main(const ArgumentList &args) {
1055 ✗ const std::string tag_name = *args.find('n')->second;
1056 ✗ const bool machine_readable = (args.find('x') != args.end());
1057
1058 // initialize the Environment (taking ownership)
1059 ✗ const bool history_read_write = false;
1060 const std::unique_ptr<Environment> env(
1061 ✗ InitializeEnvironment(args, history_read_write));
1062 ✗ if (env.get() == nullptr) {
1063 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
1064 ✗ return 1;
1065 }
1066
1067 ✗ history::History::Tag tag;
1068 ✗ const bool found = env->history->GetByName(tag_name, &tag);
1069 ✗ if (!found) {
1070 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist",
1071 tag_name.c_str());
1072 ✗ return 1;
1073 }
1074
1075 ✗ if (machine_readable) {
1076 ✗ PrintTagMachineReadable(tag);
1077 } else {
1078 ✗ PrintHumanReadableInfo(tag);
1079 }
1080
1081 ✗ return 0;
1082 }
1083
1084 //------------------------------------------------------------------------------
1085
1086 ✗ ParameterList CommandRollbackTag::GetParams() const {
1087 ✗ ParameterList r;
1088 ✗ InsertCommonParameters(&r);
1089
1090 ✗ r.push_back(Parameter::Optional('n', "name of the tag to be republished"));
1091 ✗ return r;
1092 }
1093
1094 ✗ int CommandRollbackTag::Main(const ArgumentList &args) {
1095 ✗ const bool undo_rollback = (args.find('n') == args.end());
1096 ✗ const std::string tag_name = (!undo_rollback) ? *args.find('n')->second
1097 ✗ : CommandTag::kPreviousHeadTag;
1098
1099 // initialize the Environment (taking ownership)
1100 ✗ const bool history_read_write = true;
1101 const std::unique_ptr<Environment> env(
1102 ✗ InitializeEnvironment(args, history_read_write));
1103 ✗ if (env.get() == nullptr) {
1104 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
1105 ✗ return 1;
1106 }
1107
1108 // find tag to be rolled back to
1109 ✗ history::History::Tag target_tag;
1110 ✗ const bool found = env->history->GetByName(tag_name, &target_tag);
1111 ✗ if (!found) {
1112 ✗ if (undo_rollback) {
1113 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1114 "only one anonymous rollback supported - "
1115 "perhaps you want to provide a tag name?");
1116 } else {
1117 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist",
1118 tag_name.c_str());
1119 }
1120 ✗ return 1;
1121 }
1122 ✗ if (target_tag.branch != "") {
1123 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1124 "rollback is only supported on the default branch");
1125 ✗ return 1;
1126 }
1127
1128 // list the tags that will be deleted
1129 ✗ TagList affected_tags;
1130 ✗ if (!env->history->ListTagsAffectedByRollback(tag_name, &affected_tags)) {
1131 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1132 "failed to list condemned tags prior to rollback to '%s'",
1133 tag_name.c_str());
1134 ✗ return 1;
1135 }
1136
1137 // check if tag is valid to be rolled back to
1138 ✗ const uint64_t current_revision = env->manifest->revision();
1139 ✗ assert(target_tag.revision <= current_revision);
1140 ✗ if (target_tag.revision == current_revision) {
1141 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1142 "not rolling back to current head (%" PRIu64 ")",
1143 current_revision);
1144 ✗ return 1;
1145 }
1146
1147 // open the catalog to be rolled back to
1148 const UnlinkGuard catalog_path(
1149 ✗ CreateTempPath(env->tmp_path + "/catalog", 0600));
1150 ✗ const bool catalog_read_write = true;
1151 std::unique_ptr<catalog::WritableCatalog> catalog(
1152 ✗ dynamic_cast<catalog::WritableCatalog *>(
1153 ✗ GetCatalog(env->repository_url, target_tag.root_hash,
1154 ✗ catalog_path.path(), catalog_read_write)));
1155 ✗ if (catalog.get() == nullptr) {
1156 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to open catalog with hash '%s'",
1157 ✗ target_tag.root_hash.ToString().c_str());
1158 ✗ return 1;
1159 }
1160
1161 // check if the catalog has a supported schema version
1162 ✗ if (catalog->schema() < catalog::CatalogDatabase::kLatestSupportedSchema
1163 ✗ - catalog::CatalogDatabase::kSchemaEpsilon) {
1164 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
1165 "not rolling back to outdated and "
1166 "incompatible catalog schema (%.1f < %.1f)",
1167 ✗ catalog->schema(),
1168 catalog::CatalogDatabase::kLatestSupportedSchema);
1169 ✗ return 1;
1170 }
1171
1172 // update the catalog to be republished
1173 ✗ catalog->Transaction();
1174 ✗ catalog->UpdateLastModified();
1175 ✗ catalog->SetRevision(current_revision + 1);
1176 ✗ catalog->SetPreviousRevision(env->manifest->catalog_hash());
1177 ✗ catalog->Commit();
1178
1179 // Upload catalog (handing over ownership of catalog pointer)
1180 ✗ if (!UploadCatalogAndUpdateManifest(env.get(), catalog.release())) {
1181 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "catalog upload failed");
1182 ✗ return 1;
1183 }
1184
1185 // update target tag with newly published root catalog information
1186 ✗ history::History::Tag updated_target_tag(target_tag);
1187 ✗ updated_target_tag.root_hash = env->manifest->catalog_hash();
1188 ✗ updated_target_tag.size = env->manifest->catalog_size();
1189 ✗ updated_target_tag.revision = env->manifest->revision();
1190 ✗ updated_target_tag.timestamp = env->manifest->publish_timestamp();
1191 ✗ if (!env->history->Rollback(updated_target_tag)) {
1192 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to rollback history to '%s'",
1193 updated_target_tag.name.c_str());
1194 ✗ return 1;
1195 }
1196 ✗ const bool retval = env->history->Vacuum();
1197 ✗ assert(retval);
1198
1199 // set the magic undo tags
1200 ✗ if (!UpdateUndoTags(env.get(), updated_target_tag, undo_rollback)) {
1201 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to update magic undo tags");
1202 ✗ return 1;
1203 }
1204
1205 // finalize the history and upload it
1206 ✗ if (!CloseAndPublishHistory(env.get())) {
1207 ✗ return 1;
1208 }
1209
1210 // print the tags that have been removed by the rollback
1211 ✗ PrintDeletedTagList(affected_tags);
1212
1213 ✗ return 0;
1214 }
1215
1216 ✗ void CommandRollbackTag::PrintDeletedTagList(const TagList &tags) const {
1217 ✗ size_t longest_name = 0;
1218 ✗ TagList::const_iterator i = tags.begin();
1219 ✗ const TagList::const_iterator iend = tags.end();
1220 ✗ for (; i != iend; ++i) {
1221 ✗ longest_name = std::max(i->name.size(), longest_name);
1222 }
1223
1224 ✗ i = tags.begin();
1225 ✗ for (; i != iend; ++i) {
1226 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "removed tag %s (%s)",
1227 ✗ AddPadding(i->name, longest_name).c_str(),
1228 ✗ i->root_hash.ToString().c_str());
1229 }
1230 }
1231
1232 //------------------------------------------------------------------------------
1233
1234 ✗ ParameterList CommandEmptyRecycleBin::GetParams() const {
1235 ✗ ParameterList r;
1236 ✗ InsertCommonParameters(&r);
1237 ✗ return r;
1238 }
1239
1240 ✗ int CommandEmptyRecycleBin::Main(const ArgumentList &args) {
1241 // initialize the Environment (taking ownership)
1242 ✗ const bool history_read_write = true;
1243 const std::unique_ptr<Environment> env(
1244 ✗ InitializeEnvironment(args, history_read_write));
1245 ✗ if (env.get() == nullptr) {
1246 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
1247 ✗ return 1;
1248 }
1249
1250 ✗ if (!env->history->EmptyRecycleBin()) {
1251 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to empty recycle bin");
1252 ✗ return 1;
1253 }
1254
1255 // finalize the history and upload it
1256 ✗ if (!CloseAndPublishHistory(env.get())) {
1257 ✗ return 1;
1258 }
1259
1260 ✗ return 0;
1261 }
1262