git-shuffle

Randomize Git commit timestamps to enhance privacy

git clone https://git.8pit.net/git-shuffle.git

  1#include <err.h>
  2#include <libgen.h>
  3#include <libgen.h>
  4#include <limits.h>
  5#include <stdbool.h>
  6#include <stdint.h>
  7#include <stdio.h>
  8#include <stdlib.h>
  9#include <string.h>
 10#include <time.h>
 11#include <unistd.h>
 12
 13#include <git2.h>
 14#include <sys/types.h>
 15
 16static bool amend_single = false;
 17static bool verbose = false;
 18static git_repository *repo;
 19
 20static void
 21giterr(const char *fn)
 22{
 23	const git_error *err;
 24
 25	err = git_error_last();
 26	errx(EXIT_FAILURE, "%s failed: %s", fn, err->message);
 27}
 28
 29static void
 30usage(char *prog)
 31{
 32	const char *usage = "[-a] [-v] revspec";
 33
 34	fprintf(stderr, "USAGE: %s %s\n", basename(prog), usage);
 35	exit(EXIT_FAILURE);
 36}
 37
 38static void
 39randtime(git_time *dest, const git_time *orig)
 40{
 41	time_t t;
 42	struct tm *tm;
 43	uint8_t rbytes[3];
 44
 45	if (getentropy(&rbytes, sizeof(rbytes)) == -1)
 46		err(EXIT_FAILURE, "getentropy failed");
 47
 48	/* https://github.com/libgit2/libgit2/blob/79d0e0c10ffec81152b5b1eaeb47b59adf1d4bcc/examples/log.c#L321 */
 49	t = (time_t)orig->time + (orig->offset * 60);
 50	if (!(tm = gmtime(&t)))
 51		errx(EXIT_FAILURE, "gmtime failed");
 52
 53	/* randomize time but retain current date */
 54	tm->tm_hour = rbytes[0] % 24;
 55	tm->tm_min  = rbytes[1] % 60;
 56	tm->tm_sec  = rbytes[2] % 60;
 57
 58	/* convert broken-down time to UTC epoch and use the
 59	 * offset given in orig to convert it to a local time */
 60	memcpy(dest, orig, sizeof(git_time));
 61	if ((dest->time = timegm(tm)) == (time_t)-1)
 62		err(EXIT_FAILURE, "timegm failed");
 63	dest->time -= orig->offset * 60; /* convert back to local time */
 64}
 65
 66static git_signature *
 67redate(git_commit *commit)
 68{
 69	char buf[GIT_OID_HEXSZ + 1];
 70	git_signature *newauth;
 71	const git_signature *origauth;
 72
 73	if (verbose) {
 74		git_oid_tostr(buf, sizeof(buf), git_commit_id(commit));
 75		printf("Updating time of commit %s\n", buf);
 76	}
 77
 78	origauth = git_commit_author(commit);
 79	if (git_signature_dup(&newauth, origauth))
 80		err(EXIT_FAILURE, "git_signature_dup failed");
 81	randtime(&newauth->when, &origauth->when);
 82
 83	return newauth;
 84}
 85
 86static void
 87rebase(const char *revspec)
 88{
 89	git_oid oid;
 90	git_rebase *rebase;
 91	git_annotated_commit *upstream;
 92	git_rebase_operation *op;
 93
 94	if (git_annotated_commit_from_revspec(&upstream, repo, revspec))
 95		giterr("git_annotated_commit_from_revspec");
 96	if (git_rebase_init(&rebase, repo, NULL, upstream, NULL, NULL))
 97		giterr("git_rebase_init");
 98
 99	while (!git_rebase_next(&op, rebase)) {
100		git_commit *commit;
101		git_signature *author;
102
103		if (git_commit_lookup(&commit, repo, &op->id))
104			goto err;
105		author = redate(commit);
106		if (git_rebase_commit(&oid, rebase, author, author, NULL, NULL))
107			goto err;
108
109		git_commit_free(commit);
110		git_signature_free(author);
111	}
112
113	if (git_rebase_finish(rebase, NULL))
114		giterr("git_rebase_finish");
115	return;
116
117err:
118	warnx("rebase failed: %s", git_error_last());
119	if (git_rebase_abort(rebase))
120		giterr("git_rebase_abort");
121}
122
123static void
124amend(void)
125{
126	git_oid oidcpy;
127	git_commit *commit;
128	const git_oid *oid;
129	git_reference *head;
130	const char *refname;
131	git_signature *author;
132	git_annotated_commit *annotated;
133
134	if (git_repository_head(&head, repo))
135		giterr("git_repository_head");
136	if (git_annotated_commit_from_ref(&annotated, repo, head))
137		giterr("git_annotated_commit_from_ref");
138
139	oid = git_annotated_commit_id(annotated);
140	if (git_oid_cpy(&oidcpy, oid))
141		giterr("git_oid_cpy");
142
143	if (git_commit_lookup(&commit, repo, oid))
144		giterr("git_commit_lookup");
145	refname = git_reference_name(head);
146
147	author = redate(commit);
148	if (git_commit_amend(&oidcpy, commit, refname, author, author, NULL, NULL, NULL))
149		giterr("git_commit_amend");
150}
151
152int
153main(int argc, char **argv)
154{
155	int opt;
156	static char cwd[PATH_MAX + 1];
157	static git_buf rfp;
158
159	if (!getcwd(cwd, sizeof(cwd)))
160		err(EXIT_FAILURE, "getcwd failed");
161
162	while ((opt = getopt(argc, argv, "av")) != -1) {
163		switch (opt) {
164		case 'a':
165			amend_single = true;
166			break;
167		case 'v':
168			verbose = true;
169			break;
170		default:
171			usage(argv[0]);
172			break;
173		}
174	}
175
176	git_libgit2_init();
177	if (git_repository_discover(&rfp, cwd, 0, NULL))
178		giterr("git_repository_discover");
179	if (git_repository_open(&repo, rfp.ptr))
180		giterr("git_repository_open");
181
182	if (amend_single) {
183		amend();
184	} else {
185		if (argc <= 1 || optind >= argc)
186			usage(argv[0]);
187		rebase(argv[optind]);
188	}
189}