git

Форк
0
/
gc.c 
2908 строк · 74.6 Кб
1
/*
2
 * git gc builtin command
3
 *
4
 * Cleanup unreachable files and optimize the repository.
5
 *
6
 * Copyright (c) 2007 James Bowes
7
 *
8
 * Based on git-gc.sh, which is
9
 *
10
 * Copyright (c) 2006 Shawn O. Pearce
11
 */
12

13
#include "builtin.h"
14
#include "abspath.h"
15
#include "date.h"
16
#include "environment.h"
17
#include "hex.h"
18
#include "repository.h"
19
#include "config.h"
20
#include "tempfile.h"
21
#include "lockfile.h"
22
#include "parse-options.h"
23
#include "run-command.h"
24
#include "sigchain.h"
25
#include "strvec.h"
26
#include "commit.h"
27
#include "commit-graph.h"
28
#include "packfile.h"
29
#include "object-file.h"
30
#include "object-store-ll.h"
31
#include "pack.h"
32
#include "pack-objects.h"
33
#include "path.h"
34
#include "blob.h"
35
#include "tree.h"
36
#include "promisor-remote.h"
37
#include "refs.h"
38
#include "remote.h"
39
#include "exec-cmd.h"
40
#include "gettext.h"
41
#include "hook.h"
42
#include "setup.h"
43
#include "trace2.h"
44

45
#define FAILED_RUN "failed to run %s"
46

47
static const char * const builtin_gc_usage[] = {
48
	N_("git gc [<options>]"),
49
	NULL
50
};
51

52
static timestamp_t gc_log_expire_time;
53

54
static struct strvec reflog = STRVEC_INIT;
55
static struct strvec repack = STRVEC_INIT;
56
static struct strvec prune = STRVEC_INIT;
57
static struct strvec prune_worktrees = STRVEC_INIT;
58
static struct strvec rerere = STRVEC_INIT;
59

60
static struct tempfile *pidfile;
61
static struct lock_file log_lock;
62

63
static struct string_list pack_garbage = STRING_LIST_INIT_DUP;
64

65
static void clean_pack_garbage(void)
66
{
67
	int i;
68
	for (i = 0; i < pack_garbage.nr; i++)
69
		unlink_or_warn(pack_garbage.items[i].string);
70
	string_list_clear(&pack_garbage, 0);
71
}
72

73
static void report_pack_garbage(unsigned seen_bits, const char *path)
74
{
75
	if (seen_bits == PACKDIR_FILE_IDX)
76
		string_list_append(&pack_garbage, path);
77
}
78

79
static void process_log_file(void)
80
{
81
	struct stat st;
82
	if (fstat(get_lock_file_fd(&log_lock), &st)) {
83
		/*
84
		 * Perhaps there was an i/o error or another
85
		 * unlikely situation.  Try to make a note of
86
		 * this in gc.log along with any existing
87
		 * messages.
88
		 */
89
		int saved_errno = errno;
90
		fprintf(stderr, _("Failed to fstat %s: %s"),
91
			get_lock_file_path(&log_lock),
92
			strerror(saved_errno));
93
		fflush(stderr);
94
		commit_lock_file(&log_lock);
95
		errno = saved_errno;
96
	} else if (st.st_size) {
97
		/* There was some error recorded in the lock file */
98
		commit_lock_file(&log_lock);
99
	} else {
100
		/* No error, clean up any old gc.log */
101
		unlink(git_path("gc.log"));
102
		rollback_lock_file(&log_lock);
103
	}
104
}
105

106
static void process_log_file_at_exit(void)
107
{
108
	fflush(stderr);
109
	process_log_file();
110
}
111

112
static int gc_config_is_timestamp_never(const char *var)
113
{
114
	const char *value;
115
	timestamp_t expire;
116

117
	if (!git_config_get_value(var, &value) && value) {
118
		if (parse_expiry_date(value, &expire))
119
			die(_("failed to parse '%s' value '%s'"), var, value);
120
		return expire == 0;
121
	}
122
	return 0;
123
}
124

125
struct gc_config {
126
	int pack_refs;
127
	int prune_reflogs;
128
	int cruft_packs;
129
	unsigned long max_cruft_size;
130
	int aggressive_depth;
131
	int aggressive_window;
132
	int gc_auto_threshold;
133
	int gc_auto_pack_limit;
134
	int detach_auto;
135
	char *gc_log_expire;
136
	char *prune_expire;
137
	char *prune_worktrees_expire;
138
	char *repack_filter;
139
	char *repack_filter_to;
140
	unsigned long big_pack_threshold;
141
	unsigned long max_delta_cache_size;
142
};
143

144
#define GC_CONFIG_INIT { \
145
	.pack_refs = 1, \
146
	.prune_reflogs = 1, \
147
	.cruft_packs = 1, \
148
	.aggressive_depth = 50, \
149
	.aggressive_window = 250, \
150
	.gc_auto_threshold = 6700, \
151
	.gc_auto_pack_limit = 50, \
152
	.detach_auto = 1, \
153
	.gc_log_expire = xstrdup("1.day.ago"), \
154
	.prune_expire = xstrdup("2.weeks.ago"), \
155
	.prune_worktrees_expire = xstrdup("3.months.ago"), \
156
	.max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE, \
157
}
158

159
static void gc_config_release(struct gc_config *cfg)
160
{
161
	free(cfg->gc_log_expire);
162
	free(cfg->prune_expire);
163
	free(cfg->prune_worktrees_expire);
164
	free(cfg->repack_filter);
165
	free(cfg->repack_filter_to);
166
}
167

168
static void gc_config(struct gc_config *cfg)
169
{
170
	const char *value;
171
	char *owned = NULL;
172

173
	if (!git_config_get_value("gc.packrefs", &value)) {
174
		if (value && !strcmp(value, "notbare"))
175
			cfg->pack_refs = -1;
176
		else
177
			cfg->pack_refs = git_config_bool("gc.packrefs", value);
178
	}
179

180
	if (gc_config_is_timestamp_never("gc.reflogexpire") &&
181
	    gc_config_is_timestamp_never("gc.reflogexpireunreachable"))
182
		cfg->prune_reflogs = 0;
183

184
	git_config_get_int("gc.aggressivewindow", &cfg->aggressive_window);
185
	git_config_get_int("gc.aggressivedepth", &cfg->aggressive_depth);
186
	git_config_get_int("gc.auto", &cfg->gc_auto_threshold);
187
	git_config_get_int("gc.autopacklimit", &cfg->gc_auto_pack_limit);
188
	git_config_get_bool("gc.autodetach", &cfg->detach_auto);
189
	git_config_get_bool("gc.cruftpacks", &cfg->cruft_packs);
190
	git_config_get_ulong("gc.maxcruftsize", &cfg->max_cruft_size);
191

192
	if (!repo_config_get_expiry(the_repository, "gc.pruneexpire", &owned)) {
193
		free(cfg->prune_expire);
194
		cfg->prune_expire = owned;
195
	}
196

197
	if (!repo_config_get_expiry(the_repository, "gc.worktreepruneexpire", &owned)) {
198
		free(cfg->prune_worktrees_expire);
199
		cfg->prune_worktrees_expire = owned;
200
	}
201

202
	if (!repo_config_get_expiry(the_repository, "gc.logexpiry", &owned)) {
203
		free(cfg->gc_log_expire);
204
		cfg->gc_log_expire = owned;
205
	}
206

207
	git_config_get_ulong("gc.bigpackthreshold", &cfg->big_pack_threshold);
208
	git_config_get_ulong("pack.deltacachesize", &cfg->max_delta_cache_size);
209

210
	if (!git_config_get_string("gc.repackfilter", &owned)) {
211
		free(cfg->repack_filter);
212
		cfg->repack_filter = owned;
213
	}
214

215
	if (!git_config_get_string("gc.repackfilterto", &owned)) {
216
		free(cfg->repack_filter_to);
217
		cfg->repack_filter_to = owned;
218
	}
219

220
	git_config(git_default_config, NULL);
221
}
222

223
enum schedule_priority {
224
	SCHEDULE_NONE = 0,
225
	SCHEDULE_WEEKLY = 1,
226
	SCHEDULE_DAILY = 2,
227
	SCHEDULE_HOURLY = 3,
228
};
229

230
static enum schedule_priority parse_schedule(const char *value)
231
{
232
	if (!value)
233
		return SCHEDULE_NONE;
234
	if (!strcasecmp(value, "hourly"))
235
		return SCHEDULE_HOURLY;
236
	if (!strcasecmp(value, "daily"))
237
		return SCHEDULE_DAILY;
238
	if (!strcasecmp(value, "weekly"))
239
		return SCHEDULE_WEEKLY;
240
	return SCHEDULE_NONE;
241
}
242

243
struct maintenance_run_opts {
244
	int auto_flag;
245
	int detach;
246
	int quiet;
247
	enum schedule_priority schedule;
248
};
249
#define MAINTENANCE_RUN_OPTS_INIT { \
250
	.detach = -1, \
251
}
252

253
static int pack_refs_condition(UNUSED struct gc_config *cfg)
254
{
255
	/*
256
	 * The auto-repacking logic for refs is handled by the ref backends and
257
	 * exposed via `git pack-refs --auto`. We thus always return truish
258
	 * here and let the backend decide for us.
259
	 */
260
	return 1;
261
}
262

263
static int maintenance_task_pack_refs(MAYBE_UNUSED struct maintenance_run_opts *opts,
264
				      UNUSED struct gc_config *cfg)
265
{
266
	struct child_process cmd = CHILD_PROCESS_INIT;
267

268
	cmd.git_cmd = 1;
269
	strvec_pushl(&cmd.args, "pack-refs", "--all", "--prune", NULL);
270
	if (opts->auto_flag)
271
		strvec_push(&cmd.args, "--auto");
272

273
	return run_command(&cmd);
274
}
275

276
static int too_many_loose_objects(struct gc_config *cfg)
277
{
278
	/*
279
	 * Quickly check if a "gc" is needed, by estimating how
280
	 * many loose objects there are.  Because SHA-1 is evenly
281
	 * distributed, we can check only one and get a reasonable
282
	 * estimate.
283
	 */
284
	DIR *dir;
285
	struct dirent *ent;
286
	int auto_threshold;
287
	int num_loose = 0;
288
	int needed = 0;
289
	const unsigned hexsz_loose = the_hash_algo->hexsz - 2;
290

291
	dir = opendir(git_path("objects/17"));
292
	if (!dir)
293
		return 0;
294

295
	auto_threshold = DIV_ROUND_UP(cfg->gc_auto_threshold, 256);
296
	while ((ent = readdir(dir)) != NULL) {
297
		if (strspn(ent->d_name, "0123456789abcdef") != hexsz_loose ||
298
		    ent->d_name[hexsz_loose] != '\0')
299
			continue;
300
		if (++num_loose > auto_threshold) {
301
			needed = 1;
302
			break;
303
		}
304
	}
305
	closedir(dir);
306
	return needed;
307
}
308

309
static struct packed_git *find_base_packs(struct string_list *packs,
310
					  unsigned long limit)
311
{
312
	struct packed_git *p, *base = NULL;
313

314
	for (p = get_all_packs(the_repository); p; p = p->next) {
315
		if (!p->pack_local || p->is_cruft)
316
			continue;
317
		if (limit) {
318
			if (p->pack_size >= limit)
319
				string_list_append(packs, p->pack_name);
320
		} else if (!base || base->pack_size < p->pack_size) {
321
			base = p;
322
		}
323
	}
324

325
	if (base)
326
		string_list_append(packs, base->pack_name);
327

328
	return base;
329
}
330

331
static int too_many_packs(struct gc_config *cfg)
332
{
333
	struct packed_git *p;
334
	int cnt;
335

336
	if (cfg->gc_auto_pack_limit <= 0)
337
		return 0;
338

339
	for (cnt = 0, p = get_all_packs(the_repository); p; p = p->next) {
340
		if (!p->pack_local)
341
			continue;
342
		if (p->pack_keep)
343
			continue;
344
		/*
345
		 * Perhaps check the size of the pack and count only
346
		 * very small ones here?
347
		 */
348
		cnt++;
349
	}
350
	return cfg->gc_auto_pack_limit < cnt;
351
}
352

353
static uint64_t total_ram(void)
354
{
355
#if defined(HAVE_SYSINFO)
356
	struct sysinfo si;
357

358
	if (!sysinfo(&si))
359
		return si.totalram;
360
#elif defined(HAVE_BSD_SYSCTL) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM))
361
	int64_t physical_memory;
362
	int mib[2];
363
	size_t length;
364

365
	mib[0] = CTL_HW;
366
# if defined(HW_MEMSIZE)
367
	mib[1] = HW_MEMSIZE;
368
# else
369
	mib[1] = HW_PHYSMEM;
370
# endif
371
	length = sizeof(int64_t);
372
	if (!sysctl(mib, 2, &physical_memory, &length, NULL, 0))
373
		return physical_memory;
374
#elif defined(GIT_WINDOWS_NATIVE)
375
	MEMORYSTATUSEX memInfo;
376

377
	memInfo.dwLength = sizeof(MEMORYSTATUSEX);
378
	if (GlobalMemoryStatusEx(&memInfo))
379
		return memInfo.ullTotalPhys;
380
#endif
381
	return 0;
382
}
383

384
static uint64_t estimate_repack_memory(struct gc_config *cfg,
385
				       struct packed_git *pack)
386
{
387
	unsigned long nr_objects = repo_approximate_object_count(the_repository);
388
	size_t os_cache, heap;
389

390
	if (!pack || !nr_objects)
391
		return 0;
392

393
	/*
394
	 * First we have to scan through at least one pack.
395
	 * Assume enough room in OS file cache to keep the entire pack
396
	 * or we may accidentally evict data of other processes from
397
	 * the cache.
398
	 */
399
	os_cache = pack->pack_size + pack->index_size;
400
	/* then pack-objects needs lots more for book keeping */
401
	heap = sizeof(struct object_entry) * nr_objects;
402
	/*
403
	 * internal rev-list --all --objects takes up some memory too,
404
	 * let's say half of it is for blobs
405
	 */
406
	heap += sizeof(struct blob) * nr_objects / 2;
407
	/*
408
	 * and the other half is for trees (commits and tags are
409
	 * usually insignificant)
410
	 */
411
	heap += sizeof(struct tree) * nr_objects / 2;
412
	/* and then obj_hash[], underestimated in fact */
413
	heap += sizeof(struct object *) * nr_objects;
414
	/* revindex is used also */
415
	heap += (sizeof(off_t) + sizeof(uint32_t)) * nr_objects;
416
	/*
417
	 * read_sha1_file() (either at delta calculation phase, or
418
	 * writing phase) also fills up the delta base cache
419
	 */
420
	heap += delta_base_cache_limit;
421
	/* and of course pack-objects has its own delta cache */
422
	heap += cfg->max_delta_cache_size;
423

424
	return os_cache + heap;
425
}
426

427
static int keep_one_pack(struct string_list_item *item, void *data UNUSED)
428
{
429
	strvec_pushf(&repack, "--keep-pack=%s", basename(item->string));
430
	return 0;
431
}
432

433
static void add_repack_all_option(struct gc_config *cfg,
434
				  struct string_list *keep_pack)
435
{
436
	if (cfg->prune_expire && !strcmp(cfg->prune_expire, "now"))
437
		strvec_push(&repack, "-a");
438
	else if (cfg->cruft_packs) {
439
		strvec_push(&repack, "--cruft");
440
		if (cfg->prune_expire)
441
			strvec_pushf(&repack, "--cruft-expiration=%s", cfg->prune_expire);
442
		if (cfg->max_cruft_size)
443
			strvec_pushf(&repack, "--max-cruft-size=%lu",
444
				     cfg->max_cruft_size);
445
	} else {
446
		strvec_push(&repack, "-A");
447
		if (cfg->prune_expire)
448
			strvec_pushf(&repack, "--unpack-unreachable=%s", cfg->prune_expire);
449
	}
450

451
	if (keep_pack)
452
		for_each_string_list(keep_pack, keep_one_pack, NULL);
453

454
	if (cfg->repack_filter && *cfg->repack_filter)
455
		strvec_pushf(&repack, "--filter=%s", cfg->repack_filter);
456
	if (cfg->repack_filter_to && *cfg->repack_filter_to)
457
		strvec_pushf(&repack, "--filter-to=%s", cfg->repack_filter_to);
458
}
459

460
static void add_repack_incremental_option(void)
461
{
462
	strvec_push(&repack, "--no-write-bitmap-index");
463
}
464

465
static int need_to_gc(struct gc_config *cfg)
466
{
467
	/*
468
	 * Setting gc.auto to 0 or negative can disable the
469
	 * automatic gc.
470
	 */
471
	if (cfg->gc_auto_threshold <= 0)
472
		return 0;
473

474
	/*
475
	 * If there are too many loose objects, but not too many
476
	 * packs, we run "repack -d -l".  If there are too many packs,
477
	 * we run "repack -A -d -l".  Otherwise we tell the caller
478
	 * there is no need.
479
	 */
480
	if (too_many_packs(cfg)) {
481
		struct string_list keep_pack = STRING_LIST_INIT_NODUP;
482

483
		if (cfg->big_pack_threshold) {
484
			find_base_packs(&keep_pack, cfg->big_pack_threshold);
485
			if (keep_pack.nr >= cfg->gc_auto_pack_limit) {
486
				cfg->big_pack_threshold = 0;
487
				string_list_clear(&keep_pack, 0);
488
				find_base_packs(&keep_pack, 0);
489
			}
490
		} else {
491
			struct packed_git *p = find_base_packs(&keep_pack, 0);
492
			uint64_t mem_have, mem_want;
493

494
			mem_have = total_ram();
495
			mem_want = estimate_repack_memory(cfg, p);
496

497
			/*
498
			 * Only allow 1/2 of memory for pack-objects, leave
499
			 * the rest for the OS and other processes in the
500
			 * system.
501
			 */
502
			if (!mem_have || mem_want < mem_have / 2)
503
				string_list_clear(&keep_pack, 0);
504
		}
505

506
		add_repack_all_option(cfg, &keep_pack);
507
		string_list_clear(&keep_pack, 0);
508
	} else if (too_many_loose_objects(cfg))
509
		add_repack_incremental_option();
510
	else
511
		return 0;
512

513
	if (run_hooks(the_repository, "pre-auto-gc"))
514
		return 0;
515
	return 1;
516
}
517

518
/* return NULL on success, else hostname running the gc */
519
static const char *lock_repo_for_gc(int force, pid_t* ret_pid)
520
{
521
	struct lock_file lock = LOCK_INIT;
522
	char my_host[HOST_NAME_MAX + 1];
523
	struct strbuf sb = STRBUF_INIT;
524
	struct stat st;
525
	uintmax_t pid;
526
	FILE *fp;
527
	int fd;
528
	char *pidfile_path;
529

530
	if (is_tempfile_active(pidfile))
531
		/* already locked */
532
		return NULL;
533

534
	if (xgethostname(my_host, sizeof(my_host)))
535
		xsnprintf(my_host, sizeof(my_host), "unknown");
536

537
	pidfile_path = git_pathdup("gc.pid");
538
	fd = hold_lock_file_for_update(&lock, pidfile_path,
539
				       LOCK_DIE_ON_ERROR);
540
	if (!force) {
541
		static char locking_host[HOST_NAME_MAX + 1];
542
		static char *scan_fmt;
543
		int should_exit;
544

545
		if (!scan_fmt)
546
			scan_fmt = xstrfmt("%s %%%ds", "%"SCNuMAX, HOST_NAME_MAX);
547
		fp = fopen(pidfile_path, "r");
548
		memset(locking_host, 0, sizeof(locking_host));
549
		should_exit =
550
			fp != NULL &&
551
			!fstat(fileno(fp), &st) &&
552
			/*
553
			 * 12 hour limit is very generous as gc should
554
			 * never take that long. On the other hand we
555
			 * don't really need a strict limit here,
556
			 * running gc --auto one day late is not a big
557
			 * problem. --force can be used in manual gc
558
			 * after the user verifies that no gc is
559
			 * running.
560
			 */
561
			time(NULL) - st.st_mtime <= 12 * 3600 &&
562
			fscanf(fp, scan_fmt, &pid, locking_host) == 2 &&
563
			/* be gentle to concurrent "gc" on remote hosts */
564
			(strcmp(locking_host, my_host) || !kill(pid, 0) || errno == EPERM);
565
		if (fp)
566
			fclose(fp);
567
		if (should_exit) {
568
			if (fd >= 0)
569
				rollback_lock_file(&lock);
570
			*ret_pid = pid;
571
			free(pidfile_path);
572
			return locking_host;
573
		}
574
	}
575

576
	strbuf_addf(&sb, "%"PRIuMAX" %s",
577
		    (uintmax_t) getpid(), my_host);
578
	write_in_full(fd, sb.buf, sb.len);
579
	strbuf_release(&sb);
580
	commit_lock_file(&lock);
581
	pidfile = register_tempfile(pidfile_path);
582
	free(pidfile_path);
583
	return NULL;
584
}
585

586
/*
587
 * Returns 0 if there was no previous error and gc can proceed, 1 if
588
 * gc should not proceed due to an error in the last run. Prints a
589
 * message and returns with a non-[01] status code if an error occurred
590
 * while reading gc.log
591
 */
592
static int report_last_gc_error(void)
593
{
594
	struct strbuf sb = STRBUF_INIT;
595
	int ret = 0;
596
	ssize_t len;
597
	struct stat st;
598
	char *gc_log_path = git_pathdup("gc.log");
599

600
	if (stat(gc_log_path, &st)) {
601
		if (errno == ENOENT)
602
			goto done;
603

604
		ret = die_message_errno(_("cannot stat '%s'"), gc_log_path);
605
		goto done;
606
	}
607

608
	if (st.st_mtime < gc_log_expire_time)
609
		goto done;
610

611
	len = strbuf_read_file(&sb, gc_log_path, 0);
612
	if (len < 0)
613
		ret = die_message_errno(_("cannot read '%s'"), gc_log_path);
614
	else if (len > 0) {
615
		/*
616
		 * A previous gc failed.  Report the error, and don't
617
		 * bother with an automatic gc run since it is likely
618
		 * to fail in the same way.
619
		 */
620
		warning(_("The last gc run reported the following. "
621
			       "Please correct the root cause\n"
622
			       "and remove %s\n"
623
			       "Automatic cleanup will not be performed "
624
			       "until the file is removed.\n\n"
625
			       "%s"),
626
			    gc_log_path, sb.buf);
627
		ret = 1;
628
	}
629
	strbuf_release(&sb);
630
done:
631
	free(gc_log_path);
632
	return ret;
633
}
634

635
static void gc_before_repack(struct maintenance_run_opts *opts,
636
			     struct gc_config *cfg)
637
{
638
	/*
639
	 * We may be called twice, as both the pre- and
640
	 * post-daemonized phases will call us, but running these
641
	 * commands more than once is pointless and wasteful.
642
	 */
643
	static int done = 0;
644
	if (done++)
645
		return;
646

647
	if (cfg->pack_refs && maintenance_task_pack_refs(opts, cfg))
648
		die(FAILED_RUN, "pack-refs");
649

650
	if (cfg->prune_reflogs) {
651
		struct child_process cmd = CHILD_PROCESS_INIT;
652

653
		cmd.git_cmd = 1;
654
		strvec_pushv(&cmd.args, reflog.v);
655
		if (run_command(&cmd))
656
			die(FAILED_RUN, reflog.v[0]);
657
	}
658
}
659

660
int cmd_gc(int argc, const char **argv, const char *prefix)
661
{
662
	int aggressive = 0;
663
	int quiet = 0;
664
	int force = 0;
665
	const char *name;
666
	pid_t pid;
667
	int daemonized = 0;
668
	int keep_largest_pack = -1;
669
	timestamp_t dummy;
670
	struct child_process rerere_cmd = CHILD_PROCESS_INIT;
671
	struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;
672
	struct gc_config cfg = GC_CONFIG_INIT;
673
	const char *prune_expire_sentinel = "sentinel";
674
	const char *prune_expire_arg = prune_expire_sentinel;
675
	int ret;
676

677
	struct option builtin_gc_options[] = {
678
		OPT__QUIET(&quiet, N_("suppress progress reporting")),
679
		{ OPTION_STRING, 0, "prune", &prune_expire_arg, N_("date"),
680
			N_("prune unreferenced objects"),
681
			PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg },
682
		OPT_BOOL(0, "cruft", &cfg.cruft_packs, N_("pack unreferenced objects separately")),
683
		OPT_MAGNITUDE(0, "max-cruft-size", &cfg.max_cruft_size,
684
			      N_("with --cruft, limit the size of new cruft packs")),
685
		OPT_BOOL(0, "aggressive", &aggressive, N_("be more thorough (increased runtime)")),
686
		OPT_BOOL_F(0, "auto", &opts.auto_flag, N_("enable auto-gc mode"),
687
			   PARSE_OPT_NOCOMPLETE),
688
		OPT_BOOL(0, "detach", &opts.detach,
689
			 N_("perform garbage collection in the background")),
690
		OPT_BOOL_F(0, "force", &force,
691
			   N_("force running gc even if there may be another gc running"),
692
			   PARSE_OPT_NOCOMPLETE),
693
		OPT_BOOL(0, "keep-largest-pack", &keep_largest_pack,
694
			 N_("repack all other packs except the largest pack")),
695
		OPT_END()
696
	};
697

698
	if (argc == 2 && !strcmp(argv[1], "-h"))
699
		usage_with_options(builtin_gc_usage, builtin_gc_options);
700

701
	strvec_pushl(&reflog, "reflog", "expire", "--all", NULL);
702
	strvec_pushl(&repack, "repack", "-d", "-l", NULL);
703
	strvec_pushl(&prune, "prune", "--expire", NULL);
704
	strvec_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL);
705
	strvec_pushl(&rerere, "rerere", "gc", NULL);
706

707
	gc_config(&cfg);
708

709
	if (parse_expiry_date(cfg.gc_log_expire, &gc_log_expire_time))
710
		die(_("failed to parse gc.logExpiry value %s"), cfg.gc_log_expire);
711

712
	if (cfg.pack_refs < 0)
713
		cfg.pack_refs = !is_bare_repository();
714

715
	argc = parse_options(argc, argv, prefix, builtin_gc_options,
716
			     builtin_gc_usage, 0);
717
	if (argc > 0)
718
		usage_with_options(builtin_gc_usage, builtin_gc_options);
719

720
	if (prune_expire_arg != prune_expire_sentinel) {
721
		free(cfg.prune_expire);
722
		cfg.prune_expire = xstrdup_or_null(prune_expire_arg);
723
	}
724
	if (cfg.prune_expire && parse_expiry_date(cfg.prune_expire, &dummy))
725
		die(_("failed to parse prune expiry value %s"), cfg.prune_expire);
726

727
	if (aggressive) {
728
		strvec_push(&repack, "-f");
729
		if (cfg.aggressive_depth > 0)
730
			strvec_pushf(&repack, "--depth=%d", cfg.aggressive_depth);
731
		if (cfg.aggressive_window > 0)
732
			strvec_pushf(&repack, "--window=%d", cfg.aggressive_window);
733
	}
734
	if (quiet)
735
		strvec_push(&repack, "-q");
736

737
	if (opts.auto_flag) {
738
		if (cfg.detach_auto && opts.detach < 0)
739
			opts.detach = 1;
740

741
		/*
742
		 * Auto-gc should be least intrusive as possible.
743
		 */
744
		if (!need_to_gc(&cfg)) {
745
			ret = 0;
746
			goto out;
747
		}
748

749
		if (!quiet) {
750
			if (opts.detach > 0)
751
				fprintf(stderr, _("Auto packing the repository in background for optimum performance.\n"));
752
			else
753
				fprintf(stderr, _("Auto packing the repository for optimum performance.\n"));
754
			fprintf(stderr, _("See \"git help gc\" for manual housekeeping.\n"));
755
		}
756
	} else {
757
		struct string_list keep_pack = STRING_LIST_INIT_NODUP;
758

759
		if (keep_largest_pack != -1) {
760
			if (keep_largest_pack)
761
				find_base_packs(&keep_pack, 0);
762
		} else if (cfg.big_pack_threshold) {
763
			find_base_packs(&keep_pack, cfg.big_pack_threshold);
764
		}
765

766
		add_repack_all_option(&cfg, &keep_pack);
767
		string_list_clear(&keep_pack, 0);
768
	}
769

770
	if (opts.detach > 0) {
771
		ret = report_last_gc_error();
772
		if (ret == 1) {
773
			/* Last gc --auto failed. Skip this one. */
774
			ret = 0;
775
			goto out;
776

777
		} else if (ret) {
778
			/* an I/O error occurred, already reported */
779
			goto out;
780
		}
781

782
		if (lock_repo_for_gc(force, &pid)) {
783
			ret = 0;
784
			goto out;
785
		}
786

787
		gc_before_repack(&opts, &cfg); /* dies on failure */
788
		delete_tempfile(&pidfile);
789

790
		/*
791
		 * failure to daemonize is ok, we'll continue
792
		 * in foreground
793
		 */
794
		daemonized = !daemonize();
795
	}
796

797
	name = lock_repo_for_gc(force, &pid);
798
	if (name) {
799
		if (opts.auto_flag) {
800
			ret = 0;
801
			goto out; /* be quiet on --auto */
802
		}
803

804
		die(_("gc is already running on machine '%s' pid %"PRIuMAX" (use --force if not)"),
805
		    name, (uintmax_t)pid);
806
	}
807

808
	if (daemonized) {
809
		hold_lock_file_for_update(&log_lock,
810
					  git_path("gc.log"),
811
					  LOCK_DIE_ON_ERROR);
812
		dup2(get_lock_file_fd(&log_lock), 2);
813
		atexit(process_log_file_at_exit);
814
	}
815

816
	gc_before_repack(&opts, &cfg);
817

818
	if (!repository_format_precious_objects) {
819
		struct child_process repack_cmd = CHILD_PROCESS_INIT;
820

821
		repack_cmd.git_cmd = 1;
822
		repack_cmd.close_object_store = 1;
823
		strvec_pushv(&repack_cmd.args, repack.v);
824
		if (run_command(&repack_cmd))
825
			die(FAILED_RUN, repack.v[0]);
826

827
		if (cfg.prune_expire) {
828
			struct child_process prune_cmd = CHILD_PROCESS_INIT;
829

830
			/* run `git prune` even if using cruft packs */
831
			strvec_push(&prune, cfg.prune_expire);
832
			if (quiet)
833
				strvec_push(&prune, "--no-progress");
834
			if (repo_has_promisor_remote(the_repository))
835
				strvec_push(&prune,
836
					    "--exclude-promisor-objects");
837
			prune_cmd.git_cmd = 1;
838
			strvec_pushv(&prune_cmd.args, prune.v);
839
			if (run_command(&prune_cmd))
840
				die(FAILED_RUN, prune.v[0]);
841
		}
842
	}
843

844
	if (cfg.prune_worktrees_expire) {
845
		struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT;
846

847
		strvec_push(&prune_worktrees, cfg.prune_worktrees_expire);
848
		prune_worktrees_cmd.git_cmd = 1;
849
		strvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v);
850
		if (run_command(&prune_worktrees_cmd))
851
			die(FAILED_RUN, prune_worktrees.v[0]);
852
	}
853

854
	rerere_cmd.git_cmd = 1;
855
	strvec_pushv(&rerere_cmd.args, rerere.v);
856
	if (run_command(&rerere_cmd))
857
		die(FAILED_RUN, rerere.v[0]);
858

859
	report_garbage = report_pack_garbage;
860
	reprepare_packed_git(the_repository);
861
	if (pack_garbage.nr > 0) {
862
		close_object_store(the_repository->objects);
863
		clean_pack_garbage();
864
	}
865

866
	if (the_repository->settings.gc_write_commit_graph == 1)
867
		write_commit_graph_reachable(the_repository->objects->odb,
868
					     !quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0,
869
					     NULL);
870

871
	if (opts.auto_flag && too_many_loose_objects(&cfg))
872
		warning(_("There are too many unreachable loose objects; "
873
			"run 'git prune' to remove them."));
874

875
	if (!daemonized)
876
		unlink(git_path("gc.log"));
877

878
out:
879
	gc_config_release(&cfg);
880
	return 0;
881
}
882

883
static const char *const builtin_maintenance_run_usage[] = {
884
	N_("git maintenance run [--auto] [--[no-]quiet] [--task=<task>] [--schedule]"),
885
	NULL
886
};
887

888
static int maintenance_opt_schedule(const struct option *opt, const char *arg,
889
				    int unset)
890
{
891
	enum schedule_priority *priority = opt->value;
892

893
	if (unset)
894
		die(_("--no-schedule is not allowed"));
895

896
	*priority = parse_schedule(arg);
897

898
	if (!*priority)
899
		die(_("unrecognized --schedule argument '%s'"), arg);
900

901
	return 0;
902
}
903

904
/* Remember to update object flag allocation in object.h */
905
#define SEEN		(1u<<0)
906

907
struct cg_auto_data {
908
	int num_not_in_graph;
909
	int limit;
910
};
911

912
static int dfs_on_ref(const char *refname UNUSED,
913
		      const char *referent UNUSED,
914
		      const struct object_id *oid,
915
		      int flags UNUSED,
916
		      void *cb_data)
917
{
918
	struct cg_auto_data *data = (struct cg_auto_data *)cb_data;
919
	int result = 0;
920
	struct object_id peeled;
921
	struct commit_list *stack = NULL;
922
	struct commit *commit;
923

924
	if (!peel_iterated_oid(the_repository, oid, &peeled))
925
		oid = &peeled;
926
	if (oid_object_info(the_repository, oid, NULL) != OBJ_COMMIT)
927
		return 0;
928

929
	commit = lookup_commit(the_repository, oid);
930
	if (!commit)
931
		return 0;
932
	if (repo_parse_commit(the_repository, commit) ||
933
	    commit_graph_position(commit) != COMMIT_NOT_FROM_GRAPH)
934
		return 0;
935

936
	data->num_not_in_graph++;
937

938
	if (data->num_not_in_graph >= data->limit)
939
		return 1;
940

941
	commit_list_append(commit, &stack);
942

943
	while (!result && stack) {
944
		struct commit_list *parent;
945

946
		commit = pop_commit(&stack);
947

948
		for (parent = commit->parents; parent; parent = parent->next) {
949
			if (repo_parse_commit(the_repository, parent->item) ||
950
			    commit_graph_position(parent->item) != COMMIT_NOT_FROM_GRAPH ||
951
			    parent->item->object.flags & SEEN)
952
				continue;
953

954
			parent->item->object.flags |= SEEN;
955
			data->num_not_in_graph++;
956

957
			if (data->num_not_in_graph >= data->limit) {
958
				result = 1;
959
				break;
960
			}
961

962
			commit_list_append(parent->item, &stack);
963
		}
964
	}
965

966
	free_commit_list(stack);
967
	return result;
968
}
969

970
static int should_write_commit_graph(struct gc_config *cfg)
971
{
972
	int result;
973
	struct cg_auto_data data;
974

975
	data.num_not_in_graph = 0;
976
	data.limit = 100;
977
	git_config_get_int("maintenance.commit-graph.auto",
978
			   &data.limit);
979

980
	if (!data.limit)
981
		return 0;
982
	if (data.limit < 0)
983
		return 1;
984

985
	result = refs_for_each_ref(get_main_ref_store(the_repository),
986
				   dfs_on_ref, &data);
987

988
	repo_clear_commit_marks(the_repository, SEEN);
989

990
	return result;
991
}
992

993
static int run_write_commit_graph(struct maintenance_run_opts *opts)
994
{
995
	struct child_process child = CHILD_PROCESS_INIT;
996

997
	child.git_cmd = child.close_object_store = 1;
998
	strvec_pushl(&child.args, "commit-graph", "write",
999
		     "--split", "--reachable", NULL);
1000

1001
	if (opts->quiet)
1002
		strvec_push(&child.args, "--no-progress");
1003

1004
	return !!run_command(&child);
1005
}
1006

1007
static int maintenance_task_commit_graph(struct maintenance_run_opts *opts,
1008
					 struct gc_config *cfg)
1009
{
1010
	prepare_repo_settings(the_repository);
1011
	if (!the_repository->settings.core_commit_graph)
1012
		return 0;
1013

1014
	if (run_write_commit_graph(opts)) {
1015
		error(_("failed to write commit-graph"));
1016
		return 1;
1017
	}
1018

1019
	return 0;
1020
}
1021

1022
static int fetch_remote(struct remote *remote, void *cbdata)
1023
{
1024
	struct maintenance_run_opts *opts = cbdata;
1025
	struct child_process child = CHILD_PROCESS_INIT;
1026

1027
	if (remote->skip_default_update)
1028
		return 0;
1029

1030
	child.git_cmd = 1;
1031
	strvec_pushl(&child.args, "fetch", remote->name,
1032
		     "--prefetch", "--prune", "--no-tags",
1033
		     "--no-write-fetch-head", "--recurse-submodules=no",
1034
		     NULL);
1035

1036
	if (opts->quiet)
1037
		strvec_push(&child.args, "--quiet");
1038

1039
	return !!run_command(&child);
1040
}
1041

1042
static int maintenance_task_prefetch(struct maintenance_run_opts *opts,
1043
				     struct gc_config *cfg)
1044
{
1045
	if (for_each_remote(fetch_remote, opts)) {
1046
		error(_("failed to prefetch remotes"));
1047
		return 1;
1048
	}
1049

1050
	return 0;
1051
}
1052

1053
static int maintenance_task_gc(struct maintenance_run_opts *opts,
1054
			       struct gc_config *cfg)
1055
{
1056
	struct child_process child = CHILD_PROCESS_INIT;
1057

1058
	child.git_cmd = child.close_object_store = 1;
1059
	strvec_push(&child.args, "gc");
1060

1061
	if (opts->auto_flag)
1062
		strvec_push(&child.args, "--auto");
1063
	if (opts->quiet)
1064
		strvec_push(&child.args, "--quiet");
1065
	else
1066
		strvec_push(&child.args, "--no-quiet");
1067
	strvec_push(&child.args, "--no-detach");
1068

1069
	return run_command(&child);
1070
}
1071

1072
static int prune_packed(struct maintenance_run_opts *opts)
1073
{
1074
	struct child_process child = CHILD_PROCESS_INIT;
1075

1076
	child.git_cmd = 1;
1077
	strvec_push(&child.args, "prune-packed");
1078

1079
	if (opts->quiet)
1080
		strvec_push(&child.args, "--quiet");
1081

1082
	return !!run_command(&child);
1083
}
1084

1085
struct write_loose_object_data {
1086
	FILE *in;
1087
	int count;
1088
	int batch_size;
1089
};
1090

1091
static int loose_object_auto_limit = 100;
1092

1093
static int loose_object_count(const struct object_id *oid UNUSED,
1094
			      const char *path UNUSED,
1095
			      void *data)
1096
{
1097
	int *count = (int*)data;
1098
	if (++(*count) >= loose_object_auto_limit)
1099
		return 1;
1100
	return 0;
1101
}
1102

1103
static int loose_object_auto_condition(struct gc_config *cfg)
1104
{
1105
	int count = 0;
1106

1107
	git_config_get_int("maintenance.loose-objects.auto",
1108
			   &loose_object_auto_limit);
1109

1110
	if (!loose_object_auto_limit)
1111
		return 0;
1112
	if (loose_object_auto_limit < 0)
1113
		return 1;
1114

1115
	return for_each_loose_file_in_objdir(the_repository->objects->odb->path,
1116
					     loose_object_count,
1117
					     NULL, NULL, &count);
1118
}
1119

1120
static int bail_on_loose(const struct object_id *oid UNUSED,
1121
			 const char *path UNUSED,
1122
			 void *data UNUSED)
1123
{
1124
	return 1;
1125
}
1126

1127
static int write_loose_object_to_stdin(const struct object_id *oid,
1128
				       const char *path UNUSED,
1129
				       void *data)
1130
{
1131
	struct write_loose_object_data *d = (struct write_loose_object_data *)data;
1132

1133
	fprintf(d->in, "%s\n", oid_to_hex(oid));
1134

1135
	return ++(d->count) > d->batch_size;
1136
}
1137

1138
static int pack_loose(struct maintenance_run_opts *opts)
1139
{
1140
	struct repository *r = the_repository;
1141
	int result = 0;
1142
	struct write_loose_object_data data;
1143
	struct child_process pack_proc = CHILD_PROCESS_INIT;
1144

1145
	/*
1146
	 * Do not start pack-objects process
1147
	 * if there are no loose objects.
1148
	 */
1149
	if (!for_each_loose_file_in_objdir(r->objects->odb->path,
1150
					   bail_on_loose,
1151
					   NULL, NULL, NULL))
1152
		return 0;
1153

1154
	pack_proc.git_cmd = 1;
1155

1156
	strvec_push(&pack_proc.args, "pack-objects");
1157
	if (opts->quiet)
1158
		strvec_push(&pack_proc.args, "--quiet");
1159
	strvec_pushf(&pack_proc.args, "%s/pack/loose", r->objects->odb->path);
1160

1161
	pack_proc.in = -1;
1162

1163
	/*
1164
	 * git-pack-objects(1) ends up writing the pack hash to stdout, which
1165
	 * we do not care for.
1166
	 */
1167
	pack_proc.out = -1;
1168

1169
	if (start_command(&pack_proc)) {
1170
		error(_("failed to start 'git pack-objects' process"));
1171
		return 1;
1172
	}
1173

1174
	data.in = xfdopen(pack_proc.in, "w");
1175
	data.count = 0;
1176
	data.batch_size = 50000;
1177

1178
	for_each_loose_file_in_objdir(r->objects->odb->path,
1179
				      write_loose_object_to_stdin,
1180
				      NULL,
1181
				      NULL,
1182
				      &data);
1183

1184
	fclose(data.in);
1185

1186
	if (finish_command(&pack_proc)) {
1187
		error(_("failed to finish 'git pack-objects' process"));
1188
		result = 1;
1189
	}
1190

1191
	return result;
1192
}
1193

1194
static int maintenance_task_loose_objects(struct maintenance_run_opts *opts,
1195
					  struct gc_config *cfg)
1196
{
1197
	return prune_packed(opts) || pack_loose(opts);
1198
}
1199

1200
static int incremental_repack_auto_condition(struct gc_config *cfg)
1201
{
1202
	struct packed_git *p;
1203
	int incremental_repack_auto_limit = 10;
1204
	int count = 0;
1205

1206
	prepare_repo_settings(the_repository);
1207
	if (!the_repository->settings.core_multi_pack_index)
1208
		return 0;
1209

1210
	git_config_get_int("maintenance.incremental-repack.auto",
1211
			   &incremental_repack_auto_limit);
1212

1213
	if (!incremental_repack_auto_limit)
1214
		return 0;
1215
	if (incremental_repack_auto_limit < 0)
1216
		return 1;
1217

1218
	for (p = get_packed_git(the_repository);
1219
	     count < incremental_repack_auto_limit && p;
1220
	     p = p->next) {
1221
		if (!p->multi_pack_index)
1222
			count++;
1223
	}
1224

1225
	return count >= incremental_repack_auto_limit;
1226
}
1227

1228
static int multi_pack_index_write(struct maintenance_run_opts *opts)
1229
{
1230
	struct child_process child = CHILD_PROCESS_INIT;
1231

1232
	child.git_cmd = 1;
1233
	strvec_pushl(&child.args, "multi-pack-index", "write", NULL);
1234

1235
	if (opts->quiet)
1236
		strvec_push(&child.args, "--no-progress");
1237

1238
	if (run_command(&child))
1239
		return error(_("failed to write multi-pack-index"));
1240

1241
	return 0;
1242
}
1243

1244
static int multi_pack_index_expire(struct maintenance_run_opts *opts)
1245
{
1246
	struct child_process child = CHILD_PROCESS_INIT;
1247

1248
	child.git_cmd = child.close_object_store = 1;
1249
	strvec_pushl(&child.args, "multi-pack-index", "expire", NULL);
1250

1251
	if (opts->quiet)
1252
		strvec_push(&child.args, "--no-progress");
1253

1254
	if (run_command(&child))
1255
		return error(_("'git multi-pack-index expire' failed"));
1256

1257
	return 0;
1258
}
1259

1260
#define TWO_GIGABYTES (INT32_MAX)
1261

1262
static off_t get_auto_pack_size(void)
1263
{
1264
	/*
1265
	 * The "auto" value is special: we optimize for
1266
	 * one large pack-file (i.e. from a clone) and
1267
	 * expect the rest to be small and they can be
1268
	 * repacked quickly.
1269
	 *
1270
	 * The strategy we select here is to select a
1271
	 * size that is one more than the second largest
1272
	 * pack-file. This ensures that we will repack
1273
	 * at least two packs if there are three or more
1274
	 * packs.
1275
	 */
1276
	off_t max_size = 0;
1277
	off_t second_largest_size = 0;
1278
	off_t result_size;
1279
	struct packed_git *p;
1280
	struct repository *r = the_repository;
1281

1282
	reprepare_packed_git(r);
1283
	for (p = get_all_packs(r); p; p = p->next) {
1284
		if (p->pack_size > max_size) {
1285
			second_largest_size = max_size;
1286
			max_size = p->pack_size;
1287
		} else if (p->pack_size > second_largest_size)
1288
			second_largest_size = p->pack_size;
1289
	}
1290

1291
	result_size = second_largest_size + 1;
1292

1293
	/* But limit ourselves to a batch size of 2g */
1294
	if (result_size > TWO_GIGABYTES)
1295
		result_size = TWO_GIGABYTES;
1296

1297
	return result_size;
1298
}
1299

1300
static int multi_pack_index_repack(struct maintenance_run_opts *opts)
1301
{
1302
	struct child_process child = CHILD_PROCESS_INIT;
1303

1304
	child.git_cmd = child.close_object_store = 1;
1305
	strvec_pushl(&child.args, "multi-pack-index", "repack", NULL);
1306

1307
	if (opts->quiet)
1308
		strvec_push(&child.args, "--no-progress");
1309

1310
	strvec_pushf(&child.args, "--batch-size=%"PRIuMAX,
1311
				  (uintmax_t)get_auto_pack_size());
1312

1313
	if (run_command(&child))
1314
		return error(_("'git multi-pack-index repack' failed"));
1315

1316
	return 0;
1317
}
1318

1319
static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts,
1320
					       struct gc_config *cfg)
1321
{
1322
	prepare_repo_settings(the_repository);
1323
	if (!the_repository->settings.core_multi_pack_index) {
1324
		warning(_("skipping incremental-repack task because core.multiPackIndex is disabled"));
1325
		return 0;
1326
	}
1327

1328
	if (multi_pack_index_write(opts))
1329
		return 1;
1330
	if (multi_pack_index_expire(opts))
1331
		return 1;
1332
	if (multi_pack_index_repack(opts))
1333
		return 1;
1334
	return 0;
1335
}
1336

1337
typedef int maintenance_task_fn(struct maintenance_run_opts *opts,
1338
				struct gc_config *cfg);
1339

1340
/*
1341
 * An auto condition function returns 1 if the task should run
1342
 * and 0 if the task should NOT run. See needs_to_gc() for an
1343
 * example.
1344
 */
1345
typedef int maintenance_auto_fn(struct gc_config *cfg);
1346

1347
struct maintenance_task {
1348
	const char *name;
1349
	maintenance_task_fn *fn;
1350
	maintenance_auto_fn *auto_condition;
1351
	unsigned enabled:1;
1352

1353
	enum schedule_priority schedule;
1354

1355
	/* -1 if not selected. */
1356
	int selected_order;
1357
};
1358

1359
enum maintenance_task_label {
1360
	TASK_PREFETCH,
1361
	TASK_LOOSE_OBJECTS,
1362
	TASK_INCREMENTAL_REPACK,
1363
	TASK_GC,
1364
	TASK_COMMIT_GRAPH,
1365
	TASK_PACK_REFS,
1366

1367
	/* Leave as final value */
1368
	TASK__COUNT
1369
};
1370

1371
static struct maintenance_task tasks[] = {
1372
	[TASK_PREFETCH] = {
1373
		"prefetch",
1374
		maintenance_task_prefetch,
1375
	},
1376
	[TASK_LOOSE_OBJECTS] = {
1377
		"loose-objects",
1378
		maintenance_task_loose_objects,
1379
		loose_object_auto_condition,
1380
	},
1381
	[TASK_INCREMENTAL_REPACK] = {
1382
		"incremental-repack",
1383
		maintenance_task_incremental_repack,
1384
		incremental_repack_auto_condition,
1385
	},
1386
	[TASK_GC] = {
1387
		"gc",
1388
		maintenance_task_gc,
1389
		need_to_gc,
1390
		1,
1391
	},
1392
	[TASK_COMMIT_GRAPH] = {
1393
		"commit-graph",
1394
		maintenance_task_commit_graph,
1395
		should_write_commit_graph,
1396
	},
1397
	[TASK_PACK_REFS] = {
1398
		"pack-refs",
1399
		maintenance_task_pack_refs,
1400
		pack_refs_condition,
1401
	},
1402
};
1403

1404
static int compare_tasks_by_selection(const void *a_, const void *b_)
1405
{
1406
	const struct maintenance_task *a = a_;
1407
	const struct maintenance_task *b = b_;
1408

1409
	return b->selected_order - a->selected_order;
1410
}
1411

1412
static int maintenance_run_tasks(struct maintenance_run_opts *opts,
1413
				 struct gc_config *cfg)
1414
{
1415
	int i, found_selected = 0;
1416
	int result = 0;
1417
	struct lock_file lk;
1418
	struct repository *r = the_repository;
1419
	char *lock_path = xstrfmt("%s/maintenance", r->objects->odb->path);
1420

1421
	if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {
1422
		/*
1423
		 * Another maintenance command is running.
1424
		 *
1425
		 * If --auto was provided, then it is likely due to a
1426
		 * recursive process stack. Do not report an error in
1427
		 * that case.
1428
		 */
1429
		if (!opts->auto_flag && !opts->quiet)
1430
			warning(_("lock file '%s' exists, skipping maintenance"),
1431
				lock_path);
1432
		free(lock_path);
1433
		return 0;
1434
	}
1435
	free(lock_path);
1436

1437
	/* Failure to daemonize is ok, we'll continue in foreground. */
1438
	if (opts->detach > 0) {
1439
		trace2_region_enter("maintenance", "detach", the_repository);
1440
		daemonize();
1441
		trace2_region_leave("maintenance", "detach", the_repository);
1442
	}
1443

1444
	for (i = 0; !found_selected && i < TASK__COUNT; i++)
1445
		found_selected = tasks[i].selected_order >= 0;
1446

1447
	if (found_selected)
1448
		QSORT(tasks, TASK__COUNT, compare_tasks_by_selection);
1449

1450
	for (i = 0; i < TASK__COUNT; i++) {
1451
		if (found_selected && tasks[i].selected_order < 0)
1452
			continue;
1453

1454
		if (!found_selected && !tasks[i].enabled)
1455
			continue;
1456

1457
		if (opts->auto_flag &&
1458
		    (!tasks[i].auto_condition ||
1459
		     !tasks[i].auto_condition(cfg)))
1460
			continue;
1461

1462
		if (opts->schedule && tasks[i].schedule < opts->schedule)
1463
			continue;
1464

1465
		trace2_region_enter("maintenance", tasks[i].name, r);
1466
		if (tasks[i].fn(opts, cfg)) {
1467
			error(_("task '%s' failed"), tasks[i].name);
1468
			result = 1;
1469
		}
1470
		trace2_region_leave("maintenance", tasks[i].name, r);
1471
	}
1472

1473
	rollback_lock_file(&lk);
1474
	return result;
1475
}
1476

1477
static void initialize_maintenance_strategy(void)
1478
{
1479
	char *config_str;
1480

1481
	if (git_config_get_string("maintenance.strategy", &config_str))
1482
		return;
1483

1484
	if (!strcasecmp(config_str, "incremental")) {
1485
		tasks[TASK_GC].schedule = SCHEDULE_NONE;
1486
		tasks[TASK_COMMIT_GRAPH].enabled = 1;
1487
		tasks[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY;
1488
		tasks[TASK_PREFETCH].enabled = 1;
1489
		tasks[TASK_PREFETCH].schedule = SCHEDULE_HOURLY;
1490
		tasks[TASK_INCREMENTAL_REPACK].enabled = 1;
1491
		tasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY;
1492
		tasks[TASK_LOOSE_OBJECTS].enabled = 1;
1493
		tasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY;
1494
		tasks[TASK_PACK_REFS].enabled = 1;
1495
		tasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY;
1496
	}
1497
}
1498

1499
static void initialize_task_config(int schedule)
1500
{
1501
	int i;
1502
	struct strbuf config_name = STRBUF_INIT;
1503

1504
	if (schedule)
1505
		initialize_maintenance_strategy();
1506

1507
	for (i = 0; i < TASK__COUNT; i++) {
1508
		int config_value;
1509
		char *config_str;
1510

1511
		strbuf_reset(&config_name);
1512
		strbuf_addf(&config_name, "maintenance.%s.enabled",
1513
			    tasks[i].name);
1514

1515
		if (!git_config_get_bool(config_name.buf, &config_value))
1516
			tasks[i].enabled = config_value;
1517

1518
		strbuf_reset(&config_name);
1519
		strbuf_addf(&config_name, "maintenance.%s.schedule",
1520
			    tasks[i].name);
1521

1522
		if (!git_config_get_string(config_name.buf, &config_str)) {
1523
			tasks[i].schedule = parse_schedule(config_str);
1524
			free(config_str);
1525
		}
1526
	}
1527

1528
	strbuf_release(&config_name);
1529
}
1530

1531
static int task_option_parse(const struct option *opt UNUSED,
1532
			     const char *arg, int unset)
1533
{
1534
	int i, num_selected = 0;
1535
	struct maintenance_task *task = NULL;
1536

1537
	BUG_ON_OPT_NEG(unset);
1538

1539
	for (i = 0; i < TASK__COUNT; i++) {
1540
		if (tasks[i].selected_order >= 0)
1541
			num_selected++;
1542
		if (!strcasecmp(tasks[i].name, arg)) {
1543
			task = &tasks[i];
1544
		}
1545
	}
1546

1547
	if (!task) {
1548
		error(_("'%s' is not a valid task"), arg);
1549
		return 1;
1550
	}
1551

1552
	if (task->selected_order >= 0) {
1553
		error(_("task '%s' cannot be selected multiple times"), arg);
1554
		return 1;
1555
	}
1556

1557
	task->selected_order = num_selected + 1;
1558

1559
	return 0;
1560
}
1561

1562
static int maintenance_run(int argc, const char **argv, const char *prefix)
1563
{
1564
	int i;
1565
	struct maintenance_run_opts opts = MAINTENANCE_RUN_OPTS_INIT;
1566
	struct gc_config cfg = GC_CONFIG_INIT;
1567
	struct option builtin_maintenance_run_options[] = {
1568
		OPT_BOOL(0, "auto", &opts.auto_flag,
1569
			 N_("run tasks based on the state of the repository")),
1570
		OPT_BOOL(0, "detach", &opts.detach,
1571
			 N_("perform maintenance in the background")),
1572
		OPT_CALLBACK(0, "schedule", &opts.schedule, N_("frequency"),
1573
			     N_("run tasks based on frequency"),
1574
			     maintenance_opt_schedule),
1575
		OPT_BOOL(0, "quiet", &opts.quiet,
1576
			 N_("do not report progress or other information over stderr")),
1577
		OPT_CALLBACK_F(0, "task", NULL, N_("task"),
1578
			N_("run a specific task"),
1579
			PARSE_OPT_NONEG, task_option_parse),
1580
		OPT_END()
1581
	};
1582
	int ret;
1583

1584
	opts.quiet = !isatty(2);
1585

1586
	for (i = 0; i < TASK__COUNT; i++)
1587
		tasks[i].selected_order = -1;
1588

1589
	argc = parse_options(argc, argv, prefix,
1590
			     builtin_maintenance_run_options,
1591
			     builtin_maintenance_run_usage,
1592
			     PARSE_OPT_STOP_AT_NON_OPTION);
1593

1594
	if (opts.auto_flag && opts.schedule)
1595
		die(_("use at most one of --auto and --schedule=<frequency>"));
1596

1597
	gc_config(&cfg);
1598
	initialize_task_config(opts.schedule);
1599

1600
	if (argc != 0)
1601
		usage_with_options(builtin_maintenance_run_usage,
1602
				   builtin_maintenance_run_options);
1603

1604
	ret = maintenance_run_tasks(&opts, &cfg);
1605
	gc_config_release(&cfg);
1606
	return ret;
1607
}
1608

1609
static char *get_maintpath(void)
1610
{
1611
	struct strbuf sb = STRBUF_INIT;
1612
	const char *p = the_repository->worktree ?
1613
		the_repository->worktree : the_repository->gitdir;
1614

1615
	strbuf_realpath(&sb, p, 1);
1616
	return strbuf_detach(&sb, NULL);
1617
}
1618

1619
static char const * const builtin_maintenance_register_usage[] = {
1620
	"git maintenance register [--config-file <path>]",
1621
	NULL
1622
};
1623

1624
static int maintenance_register(int argc, const char **argv, const char *prefix)
1625
{
1626
	char *config_file = NULL;
1627
	struct option options[] = {
1628
		OPT_STRING(0, "config-file", &config_file, N_("file"), N_("use given config file")),
1629
		OPT_END(),
1630
	};
1631
	int found = 0;
1632
	const char *key = "maintenance.repo";
1633
	char *maintpath = get_maintpath();
1634
	struct string_list_item *item;
1635
	const struct string_list *list;
1636

1637
	argc = parse_options(argc, argv, prefix, options,
1638
			     builtin_maintenance_register_usage, 0);
1639
	if (argc)
1640
		usage_with_options(builtin_maintenance_register_usage,
1641
				   options);
1642

1643
	/* Disable foreground maintenance */
1644
	git_config_set("maintenance.auto", "false");
1645

1646
	/* Set maintenance strategy, if unset */
1647
	if (git_config_get("maintenance.strategy"))
1648
		git_config_set("maintenance.strategy", "incremental");
1649

1650
	if (!git_config_get_string_multi(key, &list)) {
1651
		for_each_string_list_item(item, list) {
1652
			if (!strcmp(maintpath, item->string)) {
1653
				found = 1;
1654
				break;
1655
			}
1656
		}
1657
	}
1658

1659
	if (!found) {
1660
		int rc;
1661
		char *global_config_file = NULL;
1662

1663
		if (!config_file) {
1664
			global_config_file = git_global_config();
1665
			config_file = global_config_file;
1666
		}
1667
		if (!config_file)
1668
			die(_("$HOME not set"));
1669
		rc = git_config_set_multivar_in_file_gently(
1670
			config_file, "maintenance.repo", maintpath,
1671
			CONFIG_REGEX_NONE, NULL, 0);
1672
		free(global_config_file);
1673

1674
		if (rc)
1675
			die(_("unable to add '%s' value of '%s'"),
1676
			    key, maintpath);
1677
	}
1678

1679
	free(maintpath);
1680
	return 0;
1681
}
1682

1683
static char const * const builtin_maintenance_unregister_usage[] = {
1684
	"git maintenance unregister [--config-file <path>] [--force]",
1685
	NULL
1686
};
1687

1688
static int maintenance_unregister(int argc, const char **argv, const char *prefix)
1689
{
1690
	int force = 0;
1691
	char *config_file = NULL;
1692
	struct option options[] = {
1693
		OPT_STRING(0, "config-file", &config_file, N_("file"), N_("use given config file")),
1694
		OPT__FORCE(&force,
1695
			   N_("return success even if repository was not registered"),
1696
			   PARSE_OPT_NOCOMPLETE),
1697
		OPT_END(),
1698
	};
1699
	const char *key = "maintenance.repo";
1700
	char *maintpath = get_maintpath();
1701
	int found = 0;
1702
	struct string_list_item *item;
1703
	const struct string_list *list;
1704
	struct config_set cs = { { 0 } };
1705

1706
	argc = parse_options(argc, argv, prefix, options,
1707
			     builtin_maintenance_unregister_usage, 0);
1708
	if (argc)
1709
		usage_with_options(builtin_maintenance_unregister_usage,
1710
				   options);
1711

1712
	if (config_file) {
1713
		git_configset_init(&cs);
1714
		git_configset_add_file(&cs, config_file);
1715
	}
1716
	if (!(config_file
1717
	      ? git_configset_get_string_multi(&cs, key, &list)
1718
	      : git_config_get_string_multi(key, &list))) {
1719
		for_each_string_list_item(item, list) {
1720
			if (!strcmp(maintpath, item->string)) {
1721
				found = 1;
1722
				break;
1723
			}
1724
		}
1725
	}
1726

1727
	if (found) {
1728
		int rc;
1729
		char *global_config_file = NULL;
1730

1731
		if (!config_file) {
1732
			global_config_file = git_global_config();
1733
			config_file = global_config_file;
1734
		}
1735
		if (!config_file)
1736
			die(_("$HOME not set"));
1737
		rc = git_config_set_multivar_in_file_gently(
1738
			config_file, key, NULL, maintpath, NULL,
1739
			CONFIG_FLAGS_MULTI_REPLACE | CONFIG_FLAGS_FIXED_VALUE);
1740
		free(global_config_file);
1741

1742
		if (rc &&
1743
		    (!force || rc == CONFIG_NOTHING_SET))
1744
			die(_("unable to unset '%s' value of '%s'"),
1745
			    key, maintpath);
1746
	} else if (!force) {
1747
		die(_("repository '%s' is not registered"), maintpath);
1748
	}
1749

1750
	git_configset_clear(&cs);
1751
	free(maintpath);
1752
	return 0;
1753
}
1754

1755
static const char *get_frequency(enum schedule_priority schedule)
1756
{
1757
	switch (schedule) {
1758
	case SCHEDULE_HOURLY:
1759
		return "hourly";
1760
	case SCHEDULE_DAILY:
1761
		return "daily";
1762
	case SCHEDULE_WEEKLY:
1763
		return "weekly";
1764
	default:
1765
		BUG("invalid schedule %d", schedule);
1766
	}
1767
}
1768

1769
/*
1770
 * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable
1771
 * to mock the schedulers that `git maintenance start` rely on.
1772
 *
1773
 * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated
1774
 * list of colon-separated key/value pairs where each pair contains a scheduler
1775
 * and its corresponding mock.
1776
 *
1777
 * * If $GIT_TEST_MAINT_SCHEDULER is not set, return false and leave the
1778
 *   arguments unmodified.
1779
 *
1780
 * * If $GIT_TEST_MAINT_SCHEDULER is set, return true.
1781
 *   In this case, the *cmd value is read as input.
1782
 *
1783
 *   * if the input value *cmd is the key of one of the comma-separated list
1784
 *     item, then *is_available is set to true and *cmd is modified and becomes
1785
 *     the mock command.
1786
 *
1787
 *   * if the input value *cmd isn’t the key of any of the comma-separated list
1788
 *     item, then *is_available is set to false.
1789
 *
1790
 * Ex.:
1791
 *   GIT_TEST_MAINT_SCHEDULER not set
1792
 *     +-------+-------------------------------------------------+
1793
 *     | Input |                     Output                      |
1794
 *     | *cmd  | return code |       *cmd        | *is_available |
1795
 *     +-------+-------------+-------------------+---------------+
1796
 *     | "foo" |    false    | "foo" (unchanged) |  (unchanged)  |
1797
 *     +-------+-------------+-------------------+---------------+
1798
 *
1799
 *   GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh”
1800
 *     +-------+-------------------------------------------------+
1801
 *     | Input |                     Output                      |
1802
 *     | *cmd  | return code |       *cmd        | *is_available |
1803
 *     +-------+-------------+-------------------+---------------+
1804
 *     | "foo" |    true     |  "./mock.foo.sh"  |     true      |
1805
 *     | "qux" |    true     | "qux" (unchanged) |     false     |
1806
 *     +-------+-------------+-------------------+---------------+
1807
 */
1808
static int get_schedule_cmd(const char **cmd, int *is_available)
1809
{
1810
	char *testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
1811
	struct string_list_item *item;
1812
	struct string_list list = STRING_LIST_INIT_NODUP;
1813

1814
	if (!testing)
1815
		return 0;
1816

1817
	if (is_available)
1818
		*is_available = 0;
1819

1820
	string_list_split_in_place(&list, testing, ",", -1);
1821
	for_each_string_list_item(item, &list) {
1822
		struct string_list pair = STRING_LIST_INIT_NODUP;
1823

1824
		if (string_list_split_in_place(&pair, item->string, ":", 2) != 2)
1825
			continue;
1826

1827
		if (!strcmp(*cmd, pair.items[0].string)) {
1828
			*cmd = pair.items[1].string;
1829
			if (is_available)
1830
				*is_available = 1;
1831
			string_list_clear(&list, 0);
1832
			UNLEAK(testing);
1833
			return 1;
1834
		}
1835
	}
1836

1837
	string_list_clear(&list, 0);
1838
	free(testing);
1839
	return 1;
1840
}
1841

1842
static int get_random_minute(void)
1843
{
1844
	/* Use a static value when under tests. */
1845
	if (getenv("GIT_TEST_MAINT_SCHEDULER"))
1846
		return 13;
1847

1848
	return git_rand() % 60;
1849
}
1850

1851
static int is_launchctl_available(void)
1852
{
1853
	const char *cmd = "launchctl";
1854
	int is_available;
1855
	if (get_schedule_cmd(&cmd, &is_available))
1856
		return is_available;
1857

1858
#ifdef __APPLE__
1859
	return 1;
1860
#else
1861
	return 0;
1862
#endif
1863
}
1864

1865
static char *launchctl_service_name(const char *frequency)
1866
{
1867
	struct strbuf label = STRBUF_INIT;
1868
	strbuf_addf(&label, "org.git-scm.git.%s", frequency);
1869
	return strbuf_detach(&label, NULL);
1870
}
1871

1872
static char *launchctl_service_filename(const char *name)
1873
{
1874
	char *expanded;
1875
	struct strbuf filename = STRBUF_INIT;
1876
	strbuf_addf(&filename, "~/Library/LaunchAgents/%s.plist", name);
1877

1878
	expanded = interpolate_path(filename.buf, 1);
1879
	if (!expanded)
1880
		die(_("failed to expand path '%s'"), filename.buf);
1881

1882
	strbuf_release(&filename);
1883
	return expanded;
1884
}
1885

1886
static char *launchctl_get_uid(void)
1887
{
1888
	return xstrfmt("gui/%d", getuid());
1889
}
1890

1891
static int launchctl_boot_plist(int enable, const char *filename)
1892
{
1893
	const char *cmd = "launchctl";
1894
	int result;
1895
	struct child_process child = CHILD_PROCESS_INIT;
1896
	char *uid = launchctl_get_uid();
1897

1898
	get_schedule_cmd(&cmd, NULL);
1899
	strvec_split(&child.args, cmd);
1900
	strvec_pushl(&child.args, enable ? "bootstrap" : "bootout", uid,
1901
		     filename, NULL);
1902

1903
	child.no_stderr = 1;
1904
	child.no_stdout = 1;
1905

1906
	if (start_command(&child))
1907
		die(_("failed to start launchctl"));
1908

1909
	result = finish_command(&child);
1910

1911
	free(uid);
1912
	return result;
1913
}
1914

1915
static int launchctl_remove_plist(enum schedule_priority schedule)
1916
{
1917
	const char *frequency = get_frequency(schedule);
1918
	char *name = launchctl_service_name(frequency);
1919
	char *filename = launchctl_service_filename(name);
1920
	int result = launchctl_boot_plist(0, filename);
1921
	unlink(filename);
1922
	free(filename);
1923
	free(name);
1924
	return result;
1925
}
1926

1927
static int launchctl_remove_plists(void)
1928
{
1929
	return launchctl_remove_plist(SCHEDULE_HOURLY) ||
1930
	       launchctl_remove_plist(SCHEDULE_DAILY) ||
1931
	       launchctl_remove_plist(SCHEDULE_WEEKLY);
1932
}
1933

1934
static int launchctl_list_contains_plist(const char *name, const char *cmd)
1935
{
1936
	struct child_process child = CHILD_PROCESS_INIT;
1937

1938
	strvec_split(&child.args, cmd);
1939
	strvec_pushl(&child.args, "list", name, NULL);
1940

1941
	child.no_stderr = 1;
1942
	child.no_stdout = 1;
1943

1944
	if (start_command(&child))
1945
		die(_("failed to start launchctl"));
1946

1947
	/* Returns failure if 'name' doesn't exist. */
1948
	return !finish_command(&child);
1949
}
1950

1951
static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule)
1952
{
1953
	int i, fd;
1954
	const char *preamble, *repeat;
1955
	const char *frequency = get_frequency(schedule);
1956
	char *name = launchctl_service_name(frequency);
1957
	char *filename = launchctl_service_filename(name);
1958
	struct lock_file lk = LOCK_INIT;
1959
	static unsigned long lock_file_timeout_ms = ULONG_MAX;
1960
	struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT;
1961
	struct stat st;
1962
	const char *cmd = "launchctl";
1963
	int minute = get_random_minute();
1964

1965
	get_schedule_cmd(&cmd, NULL);
1966
	preamble = "<?xml version=\"1.0\"?>\n"
1967
		   "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
1968
		   "<plist version=\"1.0\">"
1969
		   "<dict>\n"
1970
		   "<key>Label</key><string>%s</string>\n"
1971
		   "<key>ProgramArguments</key>\n"
1972
		   "<array>\n"
1973
		   "<string>%s/git</string>\n"
1974
		   "<string>--exec-path=%s</string>\n"
1975
		   "<string>for-each-repo</string>\n"
1976
		   "<string>--keep-going</string>\n"
1977
		   "<string>--config=maintenance.repo</string>\n"
1978
		   "<string>maintenance</string>\n"
1979
		   "<string>run</string>\n"
1980
		   "<string>--schedule=%s</string>\n"
1981
		   "</array>\n"
1982
		   "<key>StartCalendarInterval</key>\n"
1983
		   "<array>\n";
1984
	strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency);
1985

1986
	switch (schedule) {
1987
	case SCHEDULE_HOURLY:
1988
		repeat = "<dict>\n"
1989
			 "<key>Hour</key><integer>%d</integer>\n"
1990
			 "<key>Minute</key><integer>%d</integer>\n"
1991
			 "</dict>\n";
1992
		for (i = 1; i <= 23; i++)
1993
			strbuf_addf(&plist, repeat, i, minute);
1994
		break;
1995

1996
	case SCHEDULE_DAILY:
1997
		repeat = "<dict>\n"
1998
			 "<key>Day</key><integer>%d</integer>\n"
1999
			 "<key>Hour</key><integer>0</integer>\n"
2000
			 "<key>Minute</key><integer>%d</integer>\n"
2001
			 "</dict>\n";
2002
		for (i = 1; i <= 6; i++)
2003
			strbuf_addf(&plist, repeat, i, minute);
2004
		break;
2005

2006
	case SCHEDULE_WEEKLY:
2007
		strbuf_addf(&plist,
2008
			    "<dict>\n"
2009
			    "<key>Day</key><integer>0</integer>\n"
2010
			    "<key>Hour</key><integer>0</integer>\n"
2011
			    "<key>Minute</key><integer>%d</integer>\n"
2012
			    "</dict>\n",
2013
			    minute);
2014
		break;
2015

2016
	default:
2017
		/* unreachable */
2018
		break;
2019
	}
2020
	strbuf_addstr(&plist, "</array>\n</dict>\n</plist>\n");
2021

2022
	if (safe_create_leading_directories(filename))
2023
		die(_("failed to create directories for '%s'"), filename);
2024

2025
	if ((long)lock_file_timeout_ms < 0 &&
2026
	    git_config_get_ulong("gc.launchctlplistlocktimeoutms",
2027
				 &lock_file_timeout_ms))
2028
		lock_file_timeout_ms = 150;
2029

2030
	fd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR,
2031
					       lock_file_timeout_ms);
2032

2033
	/*
2034
	 * Does this file already exist? With the intended contents? Is it
2035
	 * registered already? Then it does not need to be re-registered.
2036
	 */
2037
	if (!stat(filename, &st) && st.st_size == plist.len &&
2038
	    strbuf_read_file(&plist2, filename, plist.len) == plist.len &&
2039
	    !strbuf_cmp(&plist, &plist2) &&
2040
	    launchctl_list_contains_plist(name, cmd))
2041
		rollback_lock_file(&lk);
2042
	else {
2043
		if (write_in_full(fd, plist.buf, plist.len) < 0 ||
2044
		    commit_lock_file(&lk))
2045
			die_errno(_("could not write '%s'"), filename);
2046

2047
		/* bootout might fail if not already running, so ignore */
2048
		launchctl_boot_plist(0, filename);
2049
		if (launchctl_boot_plist(1, filename))
2050
			die(_("failed to bootstrap service %s"), filename);
2051
	}
2052

2053
	free(filename);
2054
	free(name);
2055
	strbuf_release(&plist);
2056
	strbuf_release(&plist2);
2057
	return 0;
2058
}
2059

2060
static int launchctl_add_plists(void)
2061
{
2062
	const char *exec_path = git_exec_path();
2063

2064
	return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) ||
2065
	       launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) ||
2066
	       launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY);
2067
}
2068

2069
static int launchctl_update_schedule(int run_maintenance, int fd UNUSED)
2070
{
2071
	if (run_maintenance)
2072
		return launchctl_add_plists();
2073
	else
2074
		return launchctl_remove_plists();
2075
}
2076

2077
static int is_schtasks_available(void)
2078
{
2079
	const char *cmd = "schtasks";
2080
	int is_available;
2081
	if (get_schedule_cmd(&cmd, &is_available))
2082
		return is_available;
2083

2084
#ifdef GIT_WINDOWS_NATIVE
2085
	return 1;
2086
#else
2087
	return 0;
2088
#endif
2089
}
2090

2091
static char *schtasks_task_name(const char *frequency)
2092
{
2093
	struct strbuf label = STRBUF_INIT;
2094
	strbuf_addf(&label, "Git Maintenance (%s)", frequency);
2095
	return strbuf_detach(&label, NULL);
2096
}
2097

2098
static int schtasks_remove_task(enum schedule_priority schedule)
2099
{
2100
	const char *cmd = "schtasks";
2101
	struct child_process child = CHILD_PROCESS_INIT;
2102
	const char *frequency = get_frequency(schedule);
2103
	char *name = schtasks_task_name(frequency);
2104

2105
	get_schedule_cmd(&cmd, NULL);
2106
	strvec_split(&child.args, cmd);
2107
	strvec_pushl(&child.args, "/delete", "/tn", name, "/f", NULL);
2108
	free(name);
2109

2110
	return run_command(&child);
2111
}
2112

2113
static int schtasks_remove_tasks(void)
2114
{
2115
	return schtasks_remove_task(SCHEDULE_HOURLY) ||
2116
	       schtasks_remove_task(SCHEDULE_DAILY) ||
2117
	       schtasks_remove_task(SCHEDULE_WEEKLY);
2118
}
2119

2120
static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule)
2121
{
2122
	const char *cmd = "schtasks";
2123
	int result;
2124
	struct child_process child = CHILD_PROCESS_INIT;
2125
	const char *xml;
2126
	struct tempfile *tfile;
2127
	const char *frequency = get_frequency(schedule);
2128
	char *name = schtasks_task_name(frequency);
2129
	struct strbuf tfilename = STRBUF_INIT;
2130
	int minute = get_random_minute();
2131

2132
	get_schedule_cmd(&cmd, NULL);
2133

2134
	strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX",
2135
		    get_git_common_dir(), frequency);
2136
	tfile = xmks_tempfile(tfilename.buf);
2137
	strbuf_release(&tfilename);
2138

2139
	if (!fdopen_tempfile(tfile, "w"))
2140
		die(_("failed to create temp xml file"));
2141

2142
	xml = "<?xml version=\"1.0\" ?>\n"
2143
	      "<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n"
2144
	      "<Triggers>\n"
2145
	      "<CalendarTrigger>\n";
2146
	fputs(xml, tfile->fp);
2147

2148
	switch (schedule) {
2149
	case SCHEDULE_HOURLY:
2150
		fprintf(tfile->fp,
2151
			"<StartBoundary>2020-01-01T01:%02d:00</StartBoundary>\n"
2152
			"<Enabled>true</Enabled>\n"
2153
			"<ScheduleByDay>\n"
2154
			"<DaysInterval>1</DaysInterval>\n"
2155
			"</ScheduleByDay>\n"
2156
			"<Repetition>\n"
2157
			"<Interval>PT1H</Interval>\n"
2158
			"<Duration>PT23H</Duration>\n"
2159
			"<StopAtDurationEnd>false</StopAtDurationEnd>\n"
2160
			"</Repetition>\n",
2161
			minute);
2162
		break;
2163

2164
	case SCHEDULE_DAILY:
2165
		fprintf(tfile->fp,
2166
			"<StartBoundary>2020-01-01T00:%02d:00</StartBoundary>\n"
2167
			"<Enabled>true</Enabled>\n"
2168
			"<ScheduleByWeek>\n"
2169
			"<DaysOfWeek>\n"
2170
			"<Monday />\n"
2171
			"<Tuesday />\n"
2172
			"<Wednesday />\n"
2173
			"<Thursday />\n"
2174
			"<Friday />\n"
2175
			"<Saturday />\n"
2176
			"</DaysOfWeek>\n"
2177
			"<WeeksInterval>1</WeeksInterval>\n"
2178
			"</ScheduleByWeek>\n",
2179
			minute);
2180
		break;
2181

2182
	case SCHEDULE_WEEKLY:
2183
		fprintf(tfile->fp,
2184
			"<StartBoundary>2020-01-01T00:%02d:00</StartBoundary>\n"
2185
			"<Enabled>true</Enabled>\n"
2186
			"<ScheduleByWeek>\n"
2187
			"<DaysOfWeek>\n"
2188
			"<Sunday />\n"
2189
			"</DaysOfWeek>\n"
2190
			"<WeeksInterval>1</WeeksInterval>\n"
2191
			"</ScheduleByWeek>\n",
2192
			minute);
2193
		break;
2194

2195
	default:
2196
		break;
2197
	}
2198

2199
	xml = "</CalendarTrigger>\n"
2200
	      "</Triggers>\n"
2201
	      "<Principals>\n"
2202
	      "<Principal id=\"Author\">\n"
2203
	      "<LogonType>InteractiveToken</LogonType>\n"
2204
	      "<RunLevel>LeastPrivilege</RunLevel>\n"
2205
	      "</Principal>\n"
2206
	      "</Principals>\n"
2207
	      "<Settings>\n"
2208
	      "<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
2209
	      "<Enabled>true</Enabled>\n"
2210
	      "<Hidden>true</Hidden>\n"
2211
	      "<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>\n"
2212
	      "<WakeToRun>false</WakeToRun>\n"
2213
	      "<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\n"
2214
	      "<Priority>7</Priority>\n"
2215
	      "</Settings>\n"
2216
	      "<Actions Context=\"Author\">\n"
2217
	      "<Exec>\n"
2218
	      "<Command>\"%s\\headless-git.exe\"</Command>\n"
2219
	      "<Arguments>--exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
2220
	      "</Exec>\n"
2221
	      "</Actions>\n"
2222
	      "</Task>\n";
2223
	fprintf(tfile->fp, xml, exec_path, exec_path, frequency);
2224
	strvec_split(&child.args, cmd);
2225
	strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml",
2226
				  get_tempfile_path(tfile), NULL);
2227
	close_tempfile_gently(tfile);
2228

2229
	child.no_stdout = 1;
2230
	child.no_stderr = 1;
2231

2232
	if (start_command(&child))
2233
		die(_("failed to start schtasks"));
2234
	result = finish_command(&child);
2235

2236
	delete_tempfile(&tfile);
2237
	free(name);
2238
	return result;
2239
}
2240

2241
static int schtasks_schedule_tasks(void)
2242
{
2243
	const char *exec_path = git_exec_path();
2244

2245
	return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) ||
2246
	       schtasks_schedule_task(exec_path, SCHEDULE_DAILY) ||
2247
	       schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY);
2248
}
2249

2250
static int schtasks_update_schedule(int run_maintenance, int fd UNUSED)
2251
{
2252
	if (run_maintenance)
2253
		return schtasks_schedule_tasks();
2254
	else
2255
		return schtasks_remove_tasks();
2256
}
2257

2258
MAYBE_UNUSED
2259
static int check_crontab_process(const char *cmd)
2260
{
2261
	struct child_process child = CHILD_PROCESS_INIT;
2262

2263
	strvec_split(&child.args, cmd);
2264
	strvec_push(&child.args, "-l");
2265
	child.no_stdin = 1;
2266
	child.no_stdout = 1;
2267
	child.no_stderr = 1;
2268
	child.silent_exec_failure = 1;
2269

2270
	if (start_command(&child))
2271
		return 0;
2272
	/* Ignore exit code, as an empty crontab will return error. */
2273
	finish_command(&child);
2274
	return 1;
2275
}
2276

2277
static int is_crontab_available(void)
2278
{
2279
	const char *cmd = "crontab";
2280
	int is_available;
2281

2282
	if (get_schedule_cmd(&cmd, &is_available))
2283
		return is_available;
2284

2285
#ifdef __APPLE__
2286
	/*
2287
	 * macOS has cron, but it requires special permissions and will
2288
	 * create a UI alert when attempting to run this command.
2289
	 */
2290
	return 0;
2291
#else
2292
	return check_crontab_process(cmd);
2293
#endif
2294
}
2295

2296
#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
2297
#define END_LINE "# END GIT MAINTENANCE SCHEDULE"
2298

2299
static int crontab_update_schedule(int run_maintenance, int fd)
2300
{
2301
	const char *cmd = "crontab";
2302
	int result = 0;
2303
	int in_old_region = 0;
2304
	struct child_process crontab_list = CHILD_PROCESS_INIT;
2305
	struct child_process crontab_edit = CHILD_PROCESS_INIT;
2306
	FILE *cron_list, *cron_in;
2307
	struct strbuf line = STRBUF_INIT;
2308
	struct tempfile *tmpedit = NULL;
2309
	int minute = get_random_minute();
2310

2311
	get_schedule_cmd(&cmd, NULL);
2312
	strvec_split(&crontab_list.args, cmd);
2313
	strvec_push(&crontab_list.args, "-l");
2314
	crontab_list.in = -1;
2315
	crontab_list.out = dup(fd);
2316
	crontab_list.git_cmd = 0;
2317

2318
	if (start_command(&crontab_list))
2319
		return error(_("failed to run 'crontab -l'; your system might not support 'cron'"));
2320

2321
	/* Ignore exit code, as an empty crontab will return error. */
2322
	finish_command(&crontab_list);
2323

2324
	tmpedit = mks_tempfile_t(".git_cron_edit_tmpXXXXXX");
2325
	if (!tmpedit) {
2326
		result = error(_("failed to create crontab temporary file"));
2327
		goto out;
2328
	}
2329
	cron_in = fdopen_tempfile(tmpedit, "w");
2330
	if (!cron_in) {
2331
		result = error(_("failed to open temporary file"));
2332
		goto out;
2333
	}
2334

2335
	/*
2336
	 * Read from the .lock file, filtering out the old
2337
	 * schedule while appending the new schedule.
2338
	 */
2339
	cron_list = fdopen(fd, "r");
2340
	rewind(cron_list);
2341

2342
	while (!strbuf_getline_lf(&line, cron_list)) {
2343
		if (!in_old_region && !strcmp(line.buf, BEGIN_LINE))
2344
			in_old_region = 1;
2345
		else if (in_old_region && !strcmp(line.buf, END_LINE))
2346
			in_old_region = 0;
2347
		else if (!in_old_region)
2348
			fprintf(cron_in, "%s\n", line.buf);
2349
	}
2350
	strbuf_release(&line);
2351

2352
	if (run_maintenance) {
2353
		struct strbuf line_format = STRBUF_INIT;
2354
		const char *exec_path = git_exec_path();
2355

2356
		fprintf(cron_in, "%s\n", BEGIN_LINE);
2357
		fprintf(cron_in,
2358
			"# The following schedule was created by Git\n");
2359
		fprintf(cron_in, "# Any edits made in this region might be\n");
2360
		fprintf(cron_in,
2361
			"# replaced in the future by a Git command.\n\n");
2362

2363
		strbuf_addf(&line_format,
2364
			    "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n",
2365
			    exec_path, exec_path);
2366
		fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly");
2367
		fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily");
2368
		fprintf(cron_in, line_format.buf, minute, "0", "0", "weekly");
2369
		strbuf_release(&line_format);
2370

2371
		fprintf(cron_in, "\n%s\n", END_LINE);
2372
	}
2373

2374
	fflush(cron_in);
2375

2376
	strvec_split(&crontab_edit.args, cmd);
2377
	strvec_push(&crontab_edit.args, get_tempfile_path(tmpedit));
2378
	crontab_edit.git_cmd = 0;
2379

2380
	if (start_command(&crontab_edit)) {
2381
		result = error(_("failed to run 'crontab'; your system might not support 'cron'"));
2382
		goto out;
2383
	}
2384

2385
	if (finish_command(&crontab_edit))
2386
		result = error(_("'crontab' died"));
2387
	else
2388
		fclose(cron_list);
2389
out:
2390
	delete_tempfile(&tmpedit);
2391
	return result;
2392
}
2393

2394
static int real_is_systemd_timer_available(void)
2395
{
2396
	struct child_process child = CHILD_PROCESS_INIT;
2397

2398
	strvec_pushl(&child.args, "systemctl", "--user", "list-timers", NULL);
2399
	child.no_stdin = 1;
2400
	child.no_stdout = 1;
2401
	child.no_stderr = 1;
2402
	child.silent_exec_failure = 1;
2403

2404
	if (start_command(&child))
2405
		return 0;
2406
	if (finish_command(&child))
2407
		return 0;
2408
	return 1;
2409
}
2410

2411
static int is_systemd_timer_available(void)
2412
{
2413
	const char *cmd = "systemctl";
2414
	int is_available;
2415

2416
	if (get_schedule_cmd(&cmd, &is_available))
2417
		return is_available;
2418

2419
	return real_is_systemd_timer_available();
2420
}
2421

2422
static char *xdg_config_home_systemd(const char *filename)
2423
{
2424
	return xdg_config_home_for("systemd/user", filename);
2425
}
2426

2427
#define SYSTEMD_UNIT_FORMAT "git-maintenance@%s.%s"
2428

2429
static int systemd_timer_delete_timer_file(enum schedule_priority priority)
2430
{
2431
	int ret = 0;
2432
	const char *frequency = get_frequency(priority);
2433
	char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer");
2434
	char *filename = xdg_config_home_systemd(local_timer_name);
2435

2436
	if (unlink(filename) && !is_missing_file_error(errno))
2437
		ret = error_errno(_("failed to delete '%s'"), filename);
2438

2439
	free(filename);
2440
	free(local_timer_name);
2441
	return ret;
2442
}
2443

2444
static int systemd_timer_delete_service_template(void)
2445
{
2446
	int ret = 0;
2447
	char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service");
2448
	char *filename = xdg_config_home_systemd(local_service_name);
2449
	if (unlink(filename) && !is_missing_file_error(errno))
2450
		ret = error_errno(_("failed to delete '%s'"), filename);
2451

2452
	free(filename);
2453
	free(local_service_name);
2454
	return ret;
2455
}
2456

2457
/*
2458
 * Write the schedule information into a git-maintenance@<schedule>.timer
2459
 * file using a custom minute. This timer file cannot use the templating
2460
 * system, so we generate a specific file for each.
2461
 */
2462
static int systemd_timer_write_timer_file(enum schedule_priority schedule,
2463
					  int minute)
2464
{
2465
	int res = -1;
2466
	char *filename;
2467
	FILE *file;
2468
	const char *unit;
2469
	char *schedule_pattern = NULL;
2470
	const char *frequency = get_frequency(schedule);
2471
	char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer");
2472

2473
	filename = xdg_config_home_systemd(local_timer_name);
2474

2475
	if (safe_create_leading_directories(filename)) {
2476
		error(_("failed to create directories for '%s'"), filename);
2477
		goto error;
2478
	}
2479
	file = fopen_or_warn(filename, "w");
2480
	if (!file)
2481
		goto error;
2482

2483
	switch (schedule) {
2484
	case SCHEDULE_HOURLY:
2485
		schedule_pattern = xstrfmt("*-*-* 1..23:%02d:00", minute);
2486
		break;
2487

2488
	case SCHEDULE_DAILY:
2489
		schedule_pattern = xstrfmt("Tue..Sun *-*-* 0:%02d:00", minute);
2490
		break;
2491

2492
	case SCHEDULE_WEEKLY:
2493
		schedule_pattern = xstrfmt("Mon 0:%02d:00", minute);
2494
		break;
2495

2496
	default:
2497
		BUG("Unhandled schedule_priority");
2498
	}
2499

2500
	unit = "# This file was created and is maintained by Git.\n"
2501
	       "# Any edits made in this file might be replaced in the future\n"
2502
	       "# by a Git command.\n"
2503
	       "\n"
2504
	       "[Unit]\n"
2505
	       "Description=Optimize Git repositories data\n"
2506
	       "\n"
2507
	       "[Timer]\n"
2508
	       "OnCalendar=%s\n"
2509
	       "Persistent=true\n"
2510
	       "\n"
2511
	       "[Install]\n"
2512
	       "WantedBy=timers.target\n";
2513
	if (fprintf(file, unit, schedule_pattern) < 0) {
2514
		error(_("failed to write to '%s'"), filename);
2515
		fclose(file);
2516
		goto error;
2517
	}
2518
	if (fclose(file) == EOF) {
2519
		error_errno(_("failed to flush '%s'"), filename);
2520
		goto error;
2521
	}
2522

2523
	res = 0;
2524

2525
error:
2526
	free(schedule_pattern);
2527
	free(local_timer_name);
2528
	free(filename);
2529
	return res;
2530
}
2531

2532
/*
2533
 * No matter the schedule, we use the same service and can make use of the
2534
 * templating system. When installing git-maintenance@<schedule>.timer,
2535
 * systemd will notice that git-maintenance@.service exists as a template
2536
 * and will use this file and insert the <schedule> into the template at
2537
 * the position of "%i".
2538
 */
2539
static int systemd_timer_write_service_template(const char *exec_path)
2540
{
2541
	int res = -1;
2542
	char *filename;
2543
	FILE *file;
2544
	const char *unit;
2545
	char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service");
2546

2547
	filename = xdg_config_home_systemd(local_service_name);
2548
	if (safe_create_leading_directories(filename)) {
2549
		error(_("failed to create directories for '%s'"), filename);
2550
		goto error;
2551
	}
2552
	file = fopen_or_warn(filename, "w");
2553
	if (!file)
2554
		goto error;
2555

2556
	unit = "# This file was created and is maintained by Git.\n"
2557
	       "# Any edits made in this file might be replaced in the future\n"
2558
	       "# by a Git command.\n"
2559
	       "\n"
2560
	       "[Unit]\n"
2561
	       "Description=Optimize Git repositories data\n"
2562
	       "\n"
2563
	       "[Service]\n"
2564
	       "Type=oneshot\n"
2565
	       "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n"
2566
	       "LockPersonality=yes\n"
2567
	       "MemoryDenyWriteExecute=yes\n"
2568
	       "NoNewPrivileges=yes\n"
2569
	       "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_VSOCK\n"
2570
	       "RestrictNamespaces=yes\n"
2571
	       "RestrictRealtime=yes\n"
2572
	       "RestrictSUIDSGID=yes\n"
2573
	       "SystemCallArchitectures=native\n"
2574
	       "SystemCallFilter=@system-service\n";
2575
	if (fprintf(file, unit, exec_path, exec_path) < 0) {
2576
		error(_("failed to write to '%s'"), filename);
2577
		fclose(file);
2578
		goto error;
2579
	}
2580
	if (fclose(file) == EOF) {
2581
		error_errno(_("failed to flush '%s'"), filename);
2582
		goto error;
2583
	}
2584

2585
	res = 0;
2586

2587
error:
2588
	free(local_service_name);
2589
	free(filename);
2590
	return res;
2591
}
2592

2593
static int systemd_timer_enable_unit(int enable,
2594
				     enum schedule_priority schedule,
2595
				     int minute)
2596
{
2597
	const char *cmd = "systemctl";
2598
	struct child_process child = CHILD_PROCESS_INIT;
2599
	const char *frequency = get_frequency(schedule);
2600

2601
	/*
2602
	 * Disabling the systemd unit while it is already disabled makes
2603
	 * systemctl print an error.
2604
	 * Let's ignore it since it means we already are in the expected state:
2605
	 * the unit is disabled.
2606
	 *
2607
	 * On the other hand, enabling a systemd unit which is already enabled
2608
	 * produces no error.
2609
	 */
2610
	if (!enable)
2611
		child.no_stderr = 1;
2612
	else if (systemd_timer_write_timer_file(schedule, minute))
2613
		return -1;
2614

2615
	get_schedule_cmd(&cmd, NULL);
2616
	strvec_split(&child.args, cmd);
2617
	strvec_pushl(&child.args, "--user", enable ? "enable" : "disable",
2618
		     "--now", NULL);
2619
	strvec_pushf(&child.args, SYSTEMD_UNIT_FORMAT, frequency, "timer");
2620

2621
	if (start_command(&child))
2622
		return error(_("failed to start systemctl"));
2623
	if (finish_command(&child))
2624
		/*
2625
		 * Disabling an already disabled systemd unit makes
2626
		 * systemctl fail.
2627
		 * Let's ignore this failure.
2628
		 *
2629
		 * Enabling an enabled systemd unit doesn't fail.
2630
		 */
2631
		if (enable)
2632
			return error(_("failed to run systemctl"));
2633
	return 0;
2634
}
2635

2636
/*
2637
 * A previous version of Git wrote the timer units as template files.
2638
 * Clean these up, if they exist.
2639
 */
2640
static void systemd_timer_delete_stale_timer_templates(void)
2641
{
2642
	char *timer_template_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "timer");
2643
	char *filename = xdg_config_home_systemd(timer_template_name);
2644

2645
	if (unlink(filename) && !is_missing_file_error(errno))
2646
		warning(_("failed to delete '%s'"), filename);
2647

2648
	free(filename);
2649
	free(timer_template_name);
2650
}
2651

2652
static int systemd_timer_delete_unit_files(void)
2653
{
2654
	systemd_timer_delete_stale_timer_templates();
2655

2656
	/* Purposefully not short-circuited to make sure all are called. */
2657
	return systemd_timer_delete_timer_file(SCHEDULE_HOURLY) |
2658
	       systemd_timer_delete_timer_file(SCHEDULE_DAILY) |
2659
	       systemd_timer_delete_timer_file(SCHEDULE_WEEKLY) |
2660
	       systemd_timer_delete_service_template();
2661
}
2662

2663
static int systemd_timer_delete_units(void)
2664
{
2665
	int minute = get_random_minute();
2666
	/* Purposefully not short-circuited to make sure all are called. */
2667
	return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, minute) |
2668
	       systemd_timer_enable_unit(0, SCHEDULE_DAILY, minute) |
2669
	       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, minute) |
2670
	       systemd_timer_delete_unit_files();
2671
}
2672

2673
static int systemd_timer_setup_units(void)
2674
{
2675
	int minute = get_random_minute();
2676
	const char *exec_path = git_exec_path();
2677

2678
	int ret = systemd_timer_write_service_template(exec_path) ||
2679
		  systemd_timer_enable_unit(1, SCHEDULE_HOURLY, minute) ||
2680
		  systemd_timer_enable_unit(1, SCHEDULE_DAILY, minute) ||
2681
		  systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, minute);
2682

2683
	if (ret)
2684
		systemd_timer_delete_units();
2685
	else
2686
		systemd_timer_delete_stale_timer_templates();
2687

2688
	return ret;
2689
}
2690

2691
static int systemd_timer_update_schedule(int run_maintenance, int fd UNUSED)
2692
{
2693
	if (run_maintenance)
2694
		return systemd_timer_setup_units();
2695
	else
2696
		return systemd_timer_delete_units();
2697
}
2698

2699
enum scheduler {
2700
	SCHEDULER_INVALID = -1,
2701
	SCHEDULER_AUTO,
2702
	SCHEDULER_CRON,
2703
	SCHEDULER_SYSTEMD,
2704
	SCHEDULER_LAUNCHCTL,
2705
	SCHEDULER_SCHTASKS,
2706
};
2707

2708
static const struct {
2709
	const char *name;
2710
	int (*is_available)(void);
2711
	int (*update_schedule)(int run_maintenance, int fd);
2712
} scheduler_fn[] = {
2713
	[SCHEDULER_CRON] = {
2714
		.name = "crontab",
2715
		.is_available = is_crontab_available,
2716
		.update_schedule = crontab_update_schedule,
2717
	},
2718
	[SCHEDULER_SYSTEMD] = {
2719
		.name = "systemctl",
2720
		.is_available = is_systemd_timer_available,
2721
		.update_schedule = systemd_timer_update_schedule,
2722
	},
2723
	[SCHEDULER_LAUNCHCTL] = {
2724
		.name = "launchctl",
2725
		.is_available = is_launchctl_available,
2726
		.update_schedule = launchctl_update_schedule,
2727
	},
2728
	[SCHEDULER_SCHTASKS] = {
2729
		.name = "schtasks",
2730
		.is_available = is_schtasks_available,
2731
		.update_schedule = schtasks_update_schedule,
2732
	},
2733
};
2734

2735
static enum scheduler parse_scheduler(const char *value)
2736
{
2737
	if (!value)
2738
		return SCHEDULER_INVALID;
2739
	else if (!strcasecmp(value, "auto"))
2740
		return SCHEDULER_AUTO;
2741
	else if (!strcasecmp(value, "cron") || !strcasecmp(value, "crontab"))
2742
		return SCHEDULER_CRON;
2743
	else if (!strcasecmp(value, "systemd") ||
2744
		 !strcasecmp(value, "systemd-timer"))
2745
		return SCHEDULER_SYSTEMD;
2746
	else if (!strcasecmp(value, "launchctl"))
2747
		return SCHEDULER_LAUNCHCTL;
2748
	else if (!strcasecmp(value, "schtasks"))
2749
		return SCHEDULER_SCHTASKS;
2750
	else
2751
		return SCHEDULER_INVALID;
2752
}
2753

2754
static int maintenance_opt_scheduler(const struct option *opt, const char *arg,
2755
				     int unset)
2756
{
2757
	enum scheduler *scheduler = opt->value;
2758

2759
	BUG_ON_OPT_NEG(unset);
2760

2761
	*scheduler = parse_scheduler(arg);
2762
	if (*scheduler == SCHEDULER_INVALID)
2763
		return error(_("unrecognized --scheduler argument '%s'"), arg);
2764
	return 0;
2765
}
2766

2767
struct maintenance_start_opts {
2768
	enum scheduler scheduler;
2769
};
2770

2771
static enum scheduler resolve_scheduler(enum scheduler scheduler)
2772
{
2773
	if (scheduler != SCHEDULER_AUTO)
2774
		return scheduler;
2775

2776
#if defined(__APPLE__)
2777
	return SCHEDULER_LAUNCHCTL;
2778

2779
#elif defined(GIT_WINDOWS_NATIVE)
2780
	return SCHEDULER_SCHTASKS;
2781

2782
#elif defined(__linux__)
2783
	if (is_systemd_timer_available())
2784
		return SCHEDULER_SYSTEMD;
2785
	else if (is_crontab_available())
2786
		return SCHEDULER_CRON;
2787
	else
2788
		die(_("neither systemd timers nor crontab are available"));
2789

2790
#else
2791
	return SCHEDULER_CRON;
2792
#endif
2793
}
2794

2795
static void validate_scheduler(enum scheduler scheduler)
2796
{
2797
	if (scheduler == SCHEDULER_INVALID)
2798
		BUG("invalid scheduler");
2799
	if (scheduler == SCHEDULER_AUTO)
2800
		BUG("resolve_scheduler should have been called before");
2801

2802
	if (!scheduler_fn[scheduler].is_available())
2803
		die(_("%s scheduler is not available"),
2804
		    scheduler_fn[scheduler].name);
2805
}
2806

2807
static int update_background_schedule(const struct maintenance_start_opts *opts,
2808
				      int enable)
2809
{
2810
	unsigned int i;
2811
	int result = 0;
2812
	struct lock_file lk;
2813
	char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
2814

2815
	if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {
2816
		free(lock_path);
2817
		return error(_("another process is scheduling background maintenance"));
2818
	}
2819

2820
	for (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {
2821
		if (enable && opts->scheduler == i)
2822
			continue;
2823
		if (!scheduler_fn[i].is_available())
2824
			continue;
2825
		scheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk));
2826
	}
2827

2828
	if (enable)
2829
		result = scheduler_fn[opts->scheduler].update_schedule(
2830
			1, get_lock_file_fd(&lk));
2831

2832
	rollback_lock_file(&lk);
2833

2834
	free(lock_path);
2835
	return result;
2836
}
2837

2838
static const char *const builtin_maintenance_start_usage[] = {
2839
	N_("git maintenance start [--scheduler=<scheduler>]"),
2840
	NULL
2841
};
2842

2843
static int maintenance_start(int argc, const char **argv, const char *prefix)
2844
{
2845
	struct maintenance_start_opts opts = { 0 };
2846
	struct option options[] = {
2847
		OPT_CALLBACK_F(
2848
			0, "scheduler", &opts.scheduler, N_("scheduler"),
2849
			N_("scheduler to trigger git maintenance run"),
2850
			PARSE_OPT_NONEG, maintenance_opt_scheduler),
2851
		OPT_END()
2852
	};
2853
	const char *register_args[] = { "register", NULL };
2854

2855
	argc = parse_options(argc, argv, prefix, options,
2856
			     builtin_maintenance_start_usage, 0);
2857
	if (argc)
2858
		usage_with_options(builtin_maintenance_start_usage, options);
2859

2860
	opts.scheduler = resolve_scheduler(opts.scheduler);
2861
	validate_scheduler(opts.scheduler);
2862

2863
	if (update_background_schedule(&opts, 1))
2864
		die(_("failed to set up maintenance schedule"));
2865

2866
	if (maintenance_register(ARRAY_SIZE(register_args)-1, register_args, NULL))
2867
		warning(_("failed to add repo to global config"));
2868
	return 0;
2869
}
2870

2871
static const char *const builtin_maintenance_stop_usage[] = {
2872
	"git maintenance stop",
2873
	NULL
2874
};
2875

2876
static int maintenance_stop(int argc, const char **argv, const char *prefix)
2877
{
2878
	struct option options[] = {
2879
		OPT_END()
2880
	};
2881
	argc = parse_options(argc, argv, prefix, options,
2882
			     builtin_maintenance_stop_usage, 0);
2883
	if (argc)
2884
		usage_with_options(builtin_maintenance_stop_usage, options);
2885
	return update_background_schedule(NULL, 0);
2886
}
2887

2888
static const char * const builtin_maintenance_usage[] = {
2889
	N_("git maintenance <subcommand> [<options>]"),
2890
	NULL,
2891
};
2892

2893
int cmd_maintenance(int argc, const char **argv, const char *prefix)
2894
{
2895
	parse_opt_subcommand_fn *fn = NULL;
2896
	struct option builtin_maintenance_options[] = {
2897
		OPT_SUBCOMMAND("run", &fn, maintenance_run),
2898
		OPT_SUBCOMMAND("start", &fn, maintenance_start),
2899
		OPT_SUBCOMMAND("stop", &fn, maintenance_stop),
2900
		OPT_SUBCOMMAND("register", &fn, maintenance_register),
2901
		OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister),
2902
		OPT_END(),
2903
	};
2904

2905
	argc = parse_options(argc, argv, prefix, builtin_maintenance_options,
2906
			     builtin_maintenance_usage, 0);
2907
	return fn(argc, argv, prefix);
2908
}
2909

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.