Add option preventing metastore from omitting .git dirs.
[metastore.git] / metastore.c
blobd3864f4afe43fdda04a1d2ba55fd5f0dc285d895
1 /*
2 * Main functions of the program.
4 * Copyright (C) 2007 David Härdeman <david@hardeman.nu>
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation version 2 of the License.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #define _BSD_SOURCE
22 #include <sys/types.h>
23 #include <sys/stat.h>
24 #include <getopt.h>
25 #include <utime.h>
26 #include <attr/xattr.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <unistd.h>
31 #include "metastore.h"
32 #include "utils.h"
33 #include "metaentry.h"
35 /* Used to store the path to the file containing the metadata */
36 char *metafile = METAFILE;
38 /* Used to indicate whether mtimes should be corrected */
39 static bool do_mtime = false;
41 /* Used to indicate whether empty dirs should be recreated */
42 static bool do_emptydirs = false;
44 /* Used to indicate whether .git dirs should be processed */
45 static bool do_git = false;
47 /* Used to create lists of dirs / other files which are missing in the fs */
48 static struct metaentry *missingdirs = NULL;
49 static struct metaentry *missingothers = NULL;
52 * Inserts an entry in a linked list ordered by pathlen
54 static void
55 insert_entry_plist(struct metaentry **list, struct metaentry *entry)
57 struct metaentry **parent;
59 for (parent = list; *parent; parent = &((*parent)->list)) {
60 if ((*parent)->pathlen > entry->pathlen)
61 break;
64 entry->list = *parent;
65 *parent = entry;
69 * Prints differences between real and stored actual metadata
70 * - for use in mentries_compare
72 static void
73 compare_print(struct metaentry *real, struct metaentry *stored, int cmp)
75 if (!real && !stored) {
76 msg(MSG_ERROR, "%s called with incorrect arguments\n", __FUNCTION__);
77 return;
80 if (cmp == DIFF_NONE) {
81 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
82 return;
85 msg(MSG_QUIET, "%s:\t", real ? real->path : stored->path);
87 if (cmp & DIFF_ADDED)
88 msg(MSG_QUIET, "added ", real->path);
89 if (cmp & DIFF_DELE)
90 msg(MSG_QUIET, "removed ", stored->path);
91 if (cmp & DIFF_OWNER)
92 msg(MSG_QUIET, "owner ");
93 if (cmp & DIFF_GROUP)
94 msg(MSG_QUIET, "group ");
95 if (cmp & DIFF_MODE)
96 msg(MSG_QUIET, "mode ");
97 if (cmp & DIFF_TYPE)
98 msg(MSG_QUIET, "type ");
99 if (cmp & DIFF_MTIME)
100 msg(MSG_QUIET, "mtime ");
101 if (cmp & DIFF_XATTR)
102 msg(MSG_QUIET, "xattr ");
103 msg(MSG_QUIET, "\n");
107 * Tries to change the real metadata to match the stored one
108 * - for use in mentries_compare
110 static void
111 compare_fix(struct metaentry *real, struct metaentry *stored, int cmp)
113 struct group *group;
114 struct passwd *owner;
115 gid_t gid = -1;
116 uid_t uid = -1;
117 struct utimbuf tbuf;
118 int i;
120 if (!real && !stored) {
121 msg(MSG_ERROR, "%s called with incorrect arguments\n",
122 __FUNCTION__);
123 return;
126 if (!real) {
127 if (S_ISDIR(stored->mode))
128 insert_entry_plist(&missingdirs, stored);
129 else
130 insert_entry_plist(&missingothers, stored);
132 msg(MSG_NORMAL, "%s:\tremoved\n", stored->path);
133 return;
136 if (!stored) {
137 msg(MSG_NORMAL, "%s:\tadded\n", real->path);
138 return;
141 if (cmp == DIFF_NONE) {
142 msg(MSG_DEBUG, "%s:\tno difference\n", real->path);
143 return;
146 if (cmp & DIFF_TYPE) {
147 msg(MSG_NORMAL, "%s:\tnew type, will not change metadata\n",
148 real->path);
149 return;
152 msg(MSG_QUIET, "%s:\tchanging metadata\n", real->path);
154 while (cmp & (DIFF_OWNER | DIFF_GROUP)) {
155 if (cmp & DIFF_OWNER) {
156 msg(MSG_NORMAL, "%s:\tchanging owner from %s to %s\n",
157 real->path, real->group, stored->group);
158 owner = xgetpwnam(stored->owner);
159 if (!owner) {
160 msg(MSG_DEBUG, "\tgetpwnam failed: %s\n",
161 strerror(errno));
162 break;
164 uid = owner->pw_uid;
167 if (cmp & DIFF_GROUP) {
168 msg(MSG_NORMAL, "%s:\tchanging group from %s to %s\n",
169 real->path, real->group, stored->group);
170 group = xgetgrnam(stored->group);
171 if (!group) {
172 msg(MSG_DEBUG, "\tgetgrnam failed: %s\n",
173 strerror(errno));
174 break;
176 gid = group->gr_gid;
179 if (lchown(real->path, uid, gid)) {
180 msg(MSG_DEBUG, "\tlchown failed: %s\n",
181 strerror(errno));
182 break;
184 break;
187 if (cmp & DIFF_MODE) {
188 msg(MSG_NORMAL, "%s:\tchanging mode from 0%o to 0%o\n",
189 real->path, real->mode & 07777, stored->mode & 07777);
190 if (chmod(real->path, stored->mode & 07777))
191 msg(MSG_DEBUG, "\tchmod failed: %s\n", strerror(errno));
194 /* FIXME: Use utimensat here, or even better - lutimensat */
195 if ((cmp & DIFF_MTIME) && S_ISLNK(real->mode)) {
196 msg(MSG_NORMAL, "%s:\tsymlink, not changing mtime\n", real->path);
197 } else if (cmp & DIFF_MTIME) {
198 msg(MSG_NORMAL, "%s:\tchanging mtime from %ld to %ld\n",
199 real->path, real->mtime, stored->mtime);
200 tbuf.actime = stored->mtime;
201 tbuf.modtime = stored->mtime;
202 if (utime(real->path, &tbuf)) {
203 msg(MSG_DEBUG, "\tutime failed: %s\n", strerror(errno));
204 return;
208 if (cmp & DIFF_XATTR) {
209 for (i = 0; i < real->xattrs; i++) {
210 /* Any attrs to remove? */
211 if (mentry_find_xattr(stored, real, i) >= 0)
212 continue;
214 msg(MSG_NORMAL, "%s:\tremoving xattr %s\n",
215 real->path, real->xattr_names[i]);
216 if (lremovexattr(real->path, real->xattr_names[i]))
217 msg(MSG_DEBUG, "\tlremovexattr failed: %s\n",
218 strerror(errno));
221 for (i = 0; i < stored->xattrs; i++) {
222 /* Any xattrs to add? (on change they are removed above) */
223 if (mentry_find_xattr(real, stored, i) >= 0)
224 continue;
226 msg(MSG_NORMAL, "%s:\tadding xattr %s\n",
227 stored->path, stored->xattr_names[i]);
228 if (lsetxattr(stored->path, stored->xattr_names[i],
229 stored->xattr_values[i],
230 stored->xattr_lvalues[i], XATTR_CREATE))
231 msg(MSG_DEBUG, "\tlsetxattr failed: %s\n",
232 strerror(errno));
238 * Tries to fix any empty dirs which are missing by recreating them.
239 * An "empty" dir is one which either:
240 * - is empty; or
241 * - only contained empty dirs
243 static void
244 fixup_emptydirs(struct metahash *real, struct metahash *stored)
246 struct metaentry *entry;
247 struct metaentry *cur;
248 struct metaentry **parent;
249 char *bpath;
250 char *delim;
251 size_t blen;
252 struct metaentry *new;
254 if (!missingdirs)
255 return;
256 msg(MSG_DEBUG, "\nAttempting to recreate missing dirs\n");
258 /* If directory x/y is missing, but file x/y/z is also missing,
259 * we should prune directory x/y from the list of directories to
260 * recreate since the deletition of x/y is likely to be genuine
261 * (as opposed to empty dir pruning like git/cvs does).
263 * Also, if file x/y/z is missing, any child directories of
264 * x/y should be pruned as they are probably also intentionally
265 * removed.
268 msg(MSG_DEBUG, "List of candidate dirs:\n");
269 for (cur = missingdirs; cur; cur = cur->list)
270 msg(MSG_DEBUG, " %s\n", cur->path);
272 for (entry = missingothers; entry; entry = entry->list) {
273 msg(MSG_DEBUG, "Pruning using file %s\n", entry->path);
274 bpath = xstrdup(entry->path);
275 delim = strrchr(bpath, '/');
276 if (!delim) {
277 msg(MSG_NORMAL, "No delimiter found in %s\n", bpath);
278 free(bpath);
279 continue;
281 *delim = '\0';
283 parent = &missingdirs;
284 for (cur = *parent; cur; cur = cur->list) {
285 if (strcmp(cur->path, bpath)) {
286 parent = &cur->list;
287 continue;
290 msg(MSG_DEBUG, "Prune phase 1 - %s\n", cur->path);
291 *parent = cur->list;
294 /* Now also prune subdirs of the base dir */
295 *delim++ = '/';
296 *delim = '\0';
297 blen = strlen(bpath);
299 parent = &missingdirs;
300 for (cur = *parent; cur; cur = cur->list) {
301 if (strncmp(cur->path, bpath, blen)) {
302 parent = &cur->list;
303 continue;
306 msg(MSG_DEBUG, "Prune phase 2 - %s\n", cur->path);
307 *parent = cur->list;
310 free(bpath);
312 msg(MSG_DEBUG, "\n");
314 for (cur = missingdirs; cur; cur = cur->list) {
315 msg(MSG_QUIET, "%s:\trecreating...", cur->path);
316 if (mkdir(cur->path, cur->mode)) {
317 msg(MSG_QUIET, "failed (%s)\n", strerror(errno));
318 continue;
320 msg(MSG_QUIET, "ok\n");
322 new = mentry_create(cur->path);
323 if (!new) {
324 msg(MSG_QUIET, "Failed to get metadata for %s\n");
325 continue;
328 compare_fix(new, cur, mentry_compare(new, cur, do_mtime));
332 /* Prints usage message and exits */
333 static void
334 usage(const char *arg0, const char *message)
336 if (message)
337 msg(MSG_CRITICAL, "%s: %s\n\n", arg0, message);
338 msg(MSG_CRITICAL, "Usage: %s ACTION [OPTION...] [PATH...]\n\n", arg0);
339 msg(MSG_CRITICAL, "Where ACTION is one of:\n"
340 " -c, --compare\t\tShow differences between stored and real metadata\n"
341 " -s, --save\t\tSave current metadata\n"
342 " -a, --apply\t\tApply stored metadata\n"
343 " -h, --help\t\tHelp message (this text)\n\n"
344 "Valid OPTIONS are:\n"
345 " -v, --verbose\t\tPrint more verbose messages\n"
346 " -q, --quiet\t\tPrint less verbose messages\n"
347 " -m, --mtime\t\tAlso take mtime into account for diff or apply\n"
348 " -e, --empty-dirs\tRecreate missing empty directories (experimental)\n"
349 " -g, --git\t\tDo not omit .git directories\n"
350 " -f, --file <file>\tSet metadata file\n"
353 exit(message ? EXIT_FAILURE : EXIT_SUCCESS);
356 /* Options */
357 static struct option long_options[] = {
358 {"compare", 0, 0, 0},
359 {"save", 0, 0, 0},
360 {"apply", 0, 0, 0},
361 {"help", 0, 0, 0},
362 {"verbose", 0, 0, 0},
363 {"quiet", 0, 0, 0},
364 {"mtime", 0, 0, 0},
365 {"empty-dirs", 0, 0, 0},
366 {"git", 0, 0, 0},
367 {"file", required_argument, 0, 0},
368 {0, 0, 0, 0}
371 /* Main function */
373 main(int argc, char **argv, char **envp)
375 int i, c;
376 struct metahash *real = NULL;
377 struct metahash *stored = NULL;
378 int action = 0;
380 /* Parse options */
381 i = 0;
382 while (1) {
383 int option_index = 0;
384 c = getopt_long(argc, argv, "csahvqmegf:",
385 long_options, &option_index);
386 if (c == -1)
387 break;
388 switch (c) {
389 case 0:
390 if (!strcmp("verbose",
391 long_options[option_index].name)) {
392 adjust_verbosity(1);
393 } else if (!strcmp("quiet",
394 long_options[option_index].name)) {
395 adjust_verbosity(-1);
396 } else if (!strcmp("mtime",
397 long_options[option_index].name)) {
398 do_mtime = true;
399 } else if (!strcmp("empty-dirs",
400 long_options[option_index].name)) {
401 do_emptydirs = true;
402 } else if (!strcmp("git",
403 long_options[option_index].name)) {
404 do_git = true;
405 } else if (!strcmp("file",
406 long_options[option_index].name)) {
407 metafile = optarg;
408 } else {
409 action |= (1 << option_index);
410 i++;
412 break;
413 case 'c':
414 action |= ACTION_DIFF;
415 i++;
416 break;
417 case 's':
418 action |= ACTION_SAVE;
419 i++;
420 break;
421 case 'a':
422 action |= ACTION_APPLY;
423 i++;
424 break;
425 case 'h':
426 action |= ACTION_HELP;
427 i++;
428 break;
429 case 'v':
430 adjust_verbosity(1);
431 break;
432 case 'q':
433 adjust_verbosity(-1);
434 break;
435 case 'm':
436 do_mtime = true;
437 break;
438 case 'e':
439 do_emptydirs = true;
440 break;
441 case 'g':
442 do_git = true;
443 break;
444 case 'f':
445 metafile = optarg;
446 break;
447 default:
448 usage(argv[0], "unknown option");
452 /* Make sure only one action is specified */
453 if (i != 1)
454 usage(argv[0], "incorrect option(s)");
456 /* Make sure --empty-dirs is only used with apply */
457 if (do_emptydirs && action != ACTION_APPLY)
458 usage(argv[0], "--empty-dirs is only valid with --apply");
460 /* Perform action */
461 switch (action) {
462 case ACTION_DIFF:
463 mentries_fromfile(&stored, metafile);
464 if (!stored) {
465 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
466 metafile);
467 exit(EXIT_FAILURE);
470 if (optind < argc) {
471 while (optind < argc)
472 mentries_recurse_path(argv[optind++], &real, do_git);
473 } else {
474 mentries_recurse_path(".", &real, do_git);
477 if (!real) {
478 msg(MSG_CRITICAL,
479 "Failed to load metadata from file system\n");
480 exit(EXIT_FAILURE);
483 mentries_compare(real, stored, compare_print, do_mtime);
484 break;
486 case ACTION_SAVE:
487 if (optind < argc) {
488 while (optind < argc)
489 mentries_recurse_path(argv[optind++], &real, do_git);
490 } else {
491 mentries_recurse_path(".", &real, do_git);
494 if (!real) {
495 msg(MSG_CRITICAL,
496 "Failed to load metadata from file system\n");
497 exit(EXIT_FAILURE);
500 mentries_tofile(real, metafile);
501 break;
503 case ACTION_APPLY:
504 mentries_fromfile(&stored, metafile);
505 if (!stored) {
506 msg(MSG_CRITICAL, "Failed to load metadata from %s\n",
507 metafile);
508 exit(EXIT_FAILURE);
511 if (optind < argc) {
512 while (optind < argc)
513 mentries_recurse_path(argv[optind++], &real, do_git);
514 } else {
515 mentries_recurse_path(".", &real, do_git);
518 if (!real) {
519 msg(MSG_CRITICAL,
520 "Failed to load metadata from file system\n");
521 exit(EXIT_FAILURE);
524 mentries_compare(real, stored, compare_fix, do_mtime);
526 if (do_emptydirs)
527 fixup_emptydirs(real, stored);
528 break;
530 case ACTION_HELP:
531 usage(argv[0], NULL);
534 exit(EXIT_SUCCESS);