/*
 *  SPDX-FileCopyrightText: Copyright © 2006 Keith Packard <keithp@keithp.com>
 *
 *  SPDX-License-Identifier: GPL-2.0+
 */
#include "cvs.h"
#include "revdir.h"
#include <getopt.h>
#include <regex.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>

/* options */
int commit_time_window = 300;
bool trust_commitids = true;
bool progress = false;
FILE *logfile;
char debugopts[MAXDOPTS];
#ifdef THREADS
int threads = NO_MAX;
#endif /* THREADS */

static int get_int_substr(const char *str, const regmatch_t *p) {
	char buff[256];
	if (p->rm_so == -1) {
		return 0;
	}
	if (p->rm_eo - p->rm_so >= sizeof(buff)) {
		return 0;
	}
	memcpy(buff, str + p->rm_so, p->rm_eo - p->rm_so);
	buff[p->rm_eo - p->rm_so] = 0;
	return atoi(buff);
}

static time_t mktime_utc(const struct tm *tm, const char *tzbuf) {
	/* coverity[tainted_string_return_content] */
	char *old_tz = getenv("TZ");
	time_t ret;

	setenv("TZ", tzbuf, 1);

	tzset();

	ret = mktime((struct tm *)tm);

	if (old_tz) {
		setenv("TZ", old_tz, 1);
	} else {
		unsetenv("TZ");
	}

	tzset();

	return ret;
}

/* accept a date in anything close to RFC3339 form */
static time_t convert_date(const char *dte) {
	static regex_t date_re;
	static bool init_re;

#define MAX_MATCH 16
	size_t nmatch = MAX_MATCH;
	regmatch_t match[MAX_MATCH];

	if (!init_re) {
		if (regcomp(
		        &date_re,
		        "([0-9]{4})[-/]([0-9]{2})[-/]([0-9]{2})[ "
		        "T]([0-9]{2}):([0-9]{2}):([0-9]{2})( [-+][0-9]{4})?",
		        REG_EXTENDED)) {
			fatal_error("date regex compilation error\n");
		}
		init_re = true;
	}

	if (regexec(&date_re, dte, nmatch, match, 0) == 0) {
		regmatch_t *pm = match;
		struct tm tm = {0};
		char tzbuf[32];
		int offseth, offsetm;

		/* first regmatch_t is match location of entire re */
		pm++;

		tm.tm_year = get_int_substr(dte, pm++);
		tm.tm_mon = get_int_substr(dte, pm++);
		tm.tm_mday = get_int_substr(dte, pm++);
		tm.tm_hour = get_int_substr(dte, pm++);
		tm.tm_min = get_int_substr(dte, pm++);
		tm.tm_sec = get_int_substr(dte, pm++);
		offseth = -get_int_substr(dte, pm++);

		offsetm = offseth % 100;
		if (offsetm < 0) {
			offsetm *= -1;
		}
		offseth /= 100;
		snprintf(tzbuf, sizeof(tzbuf), "UTC%+d:%d", offseth, offsetm);

		tm.tm_year -= 1900;
		tm.tm_mon--;

		return mktime_utc(&tm, tzbuf);
	} else {
		return atoi(dte);
	}
}

static void print_sizes(void) {
	printf("sizeof(char *)        = %zu\n", sizeof(char *));
	printf("sizeof(long)          = %zu\n", sizeof(long));
	printf("sizeof(int)           = %zu\n", sizeof(int));
	printf("sizeof(short)         = %zu\n", sizeof(short));
	printf("sizeof(mode_t)        = %zu\n", sizeof(mode_t));
	printf("sizeof(branchcount_t) = %zu\n", sizeof(branchcount_t));
	printf("sizeof(cvstime_t)     = %zu\n", sizeof(cvstime_t));
	printf("sizeof(time_t)        = %zu\n", sizeof(time_t));
	printf("sizeof(cvs_number)    = %zu\n", sizeof(cvs_number));
	printf("sizeof(node_t)        = %zu\n", sizeof(node_t));
	printf("sizeof(cvs_symbol)    = %zu\n", sizeof(cvs_symbol));
	printf("sizeof(cvs_branch)    = %zu\n", sizeof(cvs_branch));
	printf("sizeof(cvs_version)   = %zu\n", sizeof(cvs_version));
	printf("sizeof(cvs_patch)     = %zu\n", sizeof(cvs_patch));
	printf("sizeof(nodehash_t)    = %zu\n", sizeof(nodehash_t));
	printf("sizeof(editbuffer_t)  = %zu\n", sizeof(editbuffer_t));
	printf("sizeof(cvs_file)      = %zu\n", sizeof(cvs_file));
	printf("sizeof(rev_master)    = %zu\n", sizeof(rev_master));
	printf("sizeof(revdir)        = %zu\n", sizeof(revdir));
	printf("sizeof(cvs_commit)    = %zu\n", sizeof(cvs_commit));
	printf("sizeof(git_commit)    = %zu\n", sizeof(git_commit));
	printf("sizeof(rev_ref)       = %zu\n", sizeof(rev_ref));
	printf("sizeof(rev_list)      = %zu\n", sizeof(rev_list));
	printf("sizeof(cvs_commit_list) = %zu\n", sizeof(cvs_commit_list));
	printf("sizeof(rev_diff)      = %zu\n", sizeof(rev_diff));
	printf("sizeof(cvs_author)    = %zu\n", sizeof(cvs_author));
	printf("sizeof(chunk_t)       = %zu\n", sizeof(chunk_t));
	printf("sizeof(Tag)           = %zu\n", sizeof(tag_t));
}

struct checkpoint {
	const char *legend;
	struct timespec timespec;
	struct rusage rusage;
};

static struct checkpoint checkpoints[5];
static int ncheckpoints;

/* gather resource usage statistics */
static void gather_stats(const char *legend) {
	if (ncheckpoints >= sizeof(checkpoints) / sizeof(struct checkpoint)) {
		announce("cran out of statistics slots %s", legend);
		return;
	}

	checkpoints[ncheckpoints].legend = legend;
	(void)clock_gettime(CLOCK_REALTIME,
	                    &checkpoints[ncheckpoints].timespec);
	(void)getrusage(RUSAGE_SELF, &checkpoints[ncheckpoints].rusage);
	ncheckpoints++;
}

int main(int argc, char **argv) {
	typedef enum _execution_mode {
		ExecuteExport,
		ExecuteGraph,
		ExecuteAuthors,
	} execution_mode;

	execution_mode exec_mode = ExecuteExport;
	forest_t forest;
	export_options_t export_options = {
	    .branch_prefix = "refs/heads/",
	};
	export_stats_t export_stats;

	import_options_t import_options = {
	    .striplen = -1,
	};

#if defined(__GLIBC__)
#include <malloc.h>
	/*
	 * The default sbrk call grabs memory from the OS in 128kb chunks
	 * for malloc. As we use up memory in prodigous amounts it is
	 * better to grab it in bigger chunks, and make less calls to
	 * sbrk. 16MB seems to be a decent value.
	 */
	mallopt(M_TOP_PAD, 16 * 1024 * 1024); /* grab memory in 16MB chunks */
#endif                                        /* __GLIBC__ */
	clock_gettime(CLOCK_REALTIME, &export_options.start_time);
	memset(&export_stats, '\0', sizeof(export_stats_t));

	/* force times using mktime to be interpreted in UTC */
	setenv("TZ", "UTC", 1);

	logfile = stderr;

	while (1) {
		static const struct option options[] = {
		    {"help", 0, 0, 'h'},
		    {"version", 0, 0, 'V'},
		    {"verbose", 0, 0, 'v'},
		    {"quiet", 0, 0, 'q'},
		    {"commit-time-window", 1, 0, 'w'},
		    {"content-only", 0, 0, 'c'},
		    {"log", 1, 0, 'l'},
		    {"authormap", 1, 0, 'A'},
		    {"authorlist", 0, 0, 'a'},
		    {"revision-map", 1, 0, 'R'},
		    {"reposurgeon", 0, 0, 'r'},
		    {"graph", 0, 0, 'g'},
		    {"remote", 1, 0, 'e'},
		    {"strip", 1, 0, 's'},
		    {"progress", 0, 0, 'p'},
		    {"promiscuous", 0, 0, 'P'},
		    {"incremental", 1, 0, 'i'},
		    {"threads", 1, 0, 't'},
		    {"embed-id", 0, 0, 'E'},
		    {"sizes", 0, 0, 'S'},     /* undocumented */
		    {"noignores", 0, 0, 'N'}, /* undocumented */
		    {"debug", 1, 0, 'd'},     /* undocumented */
		    {NULL, 0, 0, '\0'},
		};
		int c = getopt_long(
		    argc, argv, "+hVw:cl:grvqaA:R:Tk:e:s:pPi:t:SENd:", options,
		    NULL);
		if (c < 0) {
			break;
		}
		switch (c) {
		case 'h':
			/* clang-format off */
			printf("Usage: cvs-fast-export [OPTIONS] [FILE]...\n"
			   "Parse RCS files and emit a fast-import stream.\n\n"
			   "Mandatory arguments to long options are mandatory for short options too.\n"
			   " -h --help                       This help\n"
			   " -g --graph                      Dump the commit graph\n"
			   " -V --version                    Print version\n"
			   " -w --commit-time-window=WINDOW  Time window for commits(seconds)\n"
			   " -c --content-only               Don't trust commit-IDs\n"
			   " -l --log=LOG_FILE               Log file\n"
			   " -a --authorlist                 Report committer IDs from repository\n"
			   " -A --authormap=AUTHOR_MAP       Author map file\n"
			   " -R --revision-map=REV_MAP       Revision map file\n"
			   " -r --reposurgeon                Issue cvs-revision properties\n"
			   " -T                              Force deterministic dates\n"
			   " -e --remote=REMOTE              Relocate branches to refs/remotes/REMOTE\n"
			   " -s --strip=PREFIX               Strip the given PREFIX instead of longest common prefix\n"
			   " -p --progress                   Enable load-status reporting\n"
			   " -P --promiscuous                Process files without ,v extension\n"
			   " -v --verbose                    Show verbose progress messages\n"
			   " -q --quiet                      Suppress normal warnings\n"
			   " -i --incremental=TIME           Incremental dump beginning after specified RFC3339-format TIME.\n"
			   " -t --threads=N                  Use threaded scheduler with N threads for CVS master analyses.\n"
			   " -E --embed-id                   Embed CVS revisions in the commit messages.\n"
			   "\n"
			   "Example: find | cvs-fast-export\n");
			/* clang-format on */
			return 0;
		case 'g':
			if (exec_mode == ExecuteGraph) {
				difffiles = true; /* Undocumented */
			}
			exec_mode = ExecuteGraph;
			break;
		case 'P':
			import_options.promiscuous = true;
			break;
		case 'v':
			import_options.verbose++;
#ifdef YYDEBUG
			extern int yydebug;
			yydebug = 1;
#endif /* YYDEBUG */
			break;
		case 'q':
			nowarn = true;
			break;
		case 'V':
			printf("%s: version " VERSION "\n", argv[0]);
			return 0;
		case 'w':
			assert(optarg);
			commit_time_window = atoi(optarg);
			break;
		case 'c':
			trust_commitids = false;
			break;
		case 'l':
			assert(optarg);
			logfile = fopen(optarg, "w");
			break;
		case 'a':
			exec_mode = ExecuteAuthors;
			break;
		case 'A':
			assert(optarg);
			if (!load_author_map(optarg)) {
				fatal_error("load of authormap file %s failed",
				            optarg);
			}
			break;
		case 'R':
			assert(optarg);
			export_options.revision_map = fopen(optarg, "w");
			if (export_options.revision_map == NULL) {
				fatal_error(
				    "cannot open %s for revision-map write",
				    optarg);
			}
			break;
		case 'r':
			export_options.reposurgeon = true;
			break;
		case 'E':
			export_options.embed_ids = true;
			break;
		case 'T':
			export_options.force_dates = true;
			break;
		case 'e':
			assert(optarg);
			export_options.branch_prefix =
			    (char *)xmalloc(strlen(optarg) + 15, __func__);
			sprintf(export_options.branch_prefix,
			        "refs/remotes/%s/", optarg);
			break;
		case 's':
			assert(optarg);
			import_options.striplen = strlen(optarg);
			if (import_options.striplen != 0) {
				import_options.striplen++;
			}
			break;
		case 'p':
			progress = true;
			break;
		case 'i':
			assert(optarg);
			export_options.fromtime = convert_date(optarg);
			noignores = true;
			break;
		case 't':
#ifdef THREADS
			assert(optarg);
			threads = atoi(optarg);
#else
			announce("not built with thread support, -t option "
			         "ignored.\n");
#endif
			break;
		case 'S':
			print_sizes();
			// cppcheck-suppress memleak
			return 0;
		case 'N':
			noignores = true;
			break;
		case 'd':
			strncpy(debugopts, optarg, sizeof(debugopts));
			break;
		default: /* error message already emitted */
			announce("try `%s --help' for more information.\n",
			         argv[0]);
			return 1;
		}
	}

	if (export_options.reposurgeon) {
		if (export_options.embed_ids) {
			fatal_error("The options --reposurgeon and --embed-id "
			            "cannot be combined.\n");
		}
	}

	argv[optind - 1] = argv[0];
	argv += optind - 1;
	argc -= optind - 1;

#ifdef THREADS
#ifdef _SC_NPROCESSORS_ONLN
	if (threads == NO_MAX) {
		threads = 2 * sysconf(_SC_NPROCESSORS_ONLN);
	}
#endif /*  _SC_NPROCESSORS_ONLN */
#endif

	gather_stats("before parsing");

	/* build CVS structures by parsing masters; may read stdin */
	analyze_masters(argc, (const char **)argv, &import_options, &forest);

	if (exec_mode == ExecuteGraph && export_options.reposurgeon) {
		dump_cvs_graph(&forest, export_options.force_dates ? 1 : 0);
		exit(0);
	}

	gather_stats("after parsing");

	/* commit set coalescence happens here */
	forest.git = collate_to_changesets(forest.cvs, forest.filecount,
	                                   import_options.verbose);

	gather_stats("after branch collation");

	/* report on the DAG */
	if (forest.git) {
		switch (exec_mode) {
		case ExecuteGraph:
			dump_rev_graph(forest.git, NULL);
			break;
		case ExecuteAuthors:
			export_authors(&forest, &export_options);
			break;
		case ExecuteExport:
			export_commits(&forest, &export_options, &export_stats);
			if (export_options.revision_map != NULL) {
				fclose(export_options.revision_map);
			}
			break;
		}
	}

	gather_stats("total");

	if (progress) {
		float elapsed;
		struct checkpoint *chp;

		for (chp = checkpoints + 1; chp < checkpoints + ncheckpoints;
		     chp++) {
			fprintf(STATUS, "%20s:\t%.3f\t%ldKB\n", chp->legend,
			        seconds_diff(&chp->timespec,
			                     &checkpoints[0].timespec),
			        chp->rusage.ru_maxrss -
			            checkpoints[0].rusage.ru_maxrss);
		}

		elapsed = seconds_diff(&checkpoints[ncheckpoints - 1].timespec,
		                       &checkpoints[0].timespec);
		fprintf(STATUS,
		        "%ld commits/%.3fM text, %u atoms at %d commits/sec.\n",
		        export_stats.export_total_commits,
		        export_stats.snapsize / 1000000.0, natoms,
		        (int)(export_stats.export_total_commits / elapsed));
	}

	if (logfile != stderr) {
		if (warncount > 0) {
			fprintf(STATUS, "cvs-fast-export: %u warning(s).\n",
			        warncount);
		}
		fclose(logfile);
	}

	discard_atoms();
	discard_tags();
	revdir_free();
	free_author_map();
	return forest.errcount > 0;
}

/* Local Variables:    */
/* mode: c             */
/* c-basic-offset: 8   */
/* indent-tabs-mode: t */
/* End:                */
