log.c 12.8 KB
Newer Older
1
/*
2
 * libgit2 "log" example - shows how to walk history and get commit info
3
 *
4 5 6 7 8 9 10 11 12
 * Written by the libgit2 contributors
 *
 * To the extent possible under law, the author(s) have dedicated all copyright
 * and related and neighboring rights to this software to the public domain
 * worldwide. This software is distributed without any warranty.
 *
 * You should have received a copy of the CC0 Public Domain Dedication along
 * with this software. If not, see
 * <http://creativecommons.org/publicdomain/zero/1.0/>.
13 14 15 16
 */

#include "common.h"

17
/**
18 19 20 21 22
 * This example demonstrates the libgit2 rev walker APIs to roughly
 * simulate the output of `git log` and a few of command line arguments.
 * `git log` has many many options and this only shows a few of them.
 *
 * This does not have:
23
 *
24 25 26 27 28
 * - Robust error handling
 * - Colorized or paginated output formatting
 * - Most of the `git log` options
 *
 * This does have:
29
 *
30 31 32 33 34
 * - Examples of translating command line arguments to equivalent libgit2
 *   revwalker configuration calls
 * - Simplified options to apply pathspec limits and to show basic diffs
 */

35
/** log_state represents walker being configured while handling options */
36
struct log_state {
37
	git_repository *repo;
38
	const char *repodir;
39
	git_revwalk *walker;
40 41
	int hide;
	int sorting;
42
	int revisions;
43
};
44

45
/** utility functions that are called to configure the walker */
46 47 48 49
static void set_sorting(struct log_state *s, unsigned int sort_mode);
static void push_rev(struct log_state *s, git_object *obj, int hide);
static int add_revision(struct log_state *s, const char *revstr);

50
/** log_options holds other command line options that affect log output */
51 52 53 54 55 56
struct log_options {
	int show_diff;
	int skip, limit;
	int min_parents, max_parents;
	git_time_t before;
	git_time_t after;
57
	const char *author;
58
	const char *committer;
Eoin Coffey committed
59
	const char *grep;
60 61
};

62
/** utility functions that parse options and help with log output */
63 64 65 66 67 68
static int parse_options(
	struct log_state *s, struct log_options *opt, int argc, char **argv);
static void print_time(const git_time *intime, const char *prefix);
static void print_commit(git_commit *commit);
static int match_with_parent(git_commit *commit, int i, git_diff_options *);

Eoin Coffey committed
69
/** utility functions for filtering */
70 71
static int signature_matches(const git_signature *sig, const char *filter);
static int log_message_matches(const git_commit *commit, const char *filter);
72 73

int main(int argc, char *argv[])
74
{
75 76 77 78 79 80 81
	int i, count = 0, printed = 0, parents, last_arg;
	struct log_state s;
	struct log_options opt;
	git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT;
	git_oid oid;
	git_commit *commit = NULL;
	git_pathspec *ps = NULL;
82

83
	git_threads_init();
84

85
	/** Parse arguments and set up revwalker. */
86

87 88 89 90 91 92 93 94 95 96 97
	last_arg = parse_options(&s, &opt, argc, argv);

	diffopts.pathspec.strings = &argv[last_arg];
	diffopts.pathspec.count	  = argc - last_arg;
	if (diffopts.pathspec.count > 0)
		check_lg2(git_pathspec_new(&ps, &diffopts.pathspec),
			"Building pathspec", NULL);

	if (!s.revisions)
		add_revision(&s, NULL);

98
	/** Use the revwalker to traverse the history. */
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134

	printed = count = 0;

	for (; !git_revwalk_next(&oid, s.walker); git_commit_free(commit)) {
		check_lg2(git_commit_lookup(&commit, s.repo, &oid),
			"Failed to look up commit", NULL);

		parents = (int)git_commit_parentcount(commit);
		if (parents < opt.min_parents)
			continue;
		if (opt.max_parents > 0 && parents > opt.max_parents)
			continue;

		if (diffopts.pathspec.count > 0) {
			int unmatched = parents;

			if (parents == 0) {
				git_tree *tree;
				check_lg2(git_commit_tree(&tree, commit), "Get tree", NULL);
				if (git_pathspec_match_tree(
						NULL, tree, GIT_PATHSPEC_NO_MATCH_ERROR, ps) != 0)
					unmatched = 1;
				git_tree_free(tree);
			} else if (parents == 1) {
				unmatched = match_with_parent(commit, 0, &diffopts) ? 0 : 1;
			} else {
				for (i = 0; i < parents; ++i) {
					if (match_with_parent(commit, i, &diffopts))
						unmatched--;
				}
			}

			if (unmatched > 0)
				continue;
		}

135
		if (!signature_matches(git_commit_author(commit), opt.author))
136 137
			continue;

138
		if (!signature_matches(git_commit_committer(commit), opt.committer))
139
			continue;
140

141
		if (!log_message_matches(commit, opt.grep))
Eoin Coffey committed
142 143
			continue;

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
		if (count++ < opt.skip)
			continue;
		if (opt.limit != -1 && printed++ >= opt.limit) {
			git_commit_free(commit);
			break;
		}

		print_commit(commit);

		if (opt.show_diff) {
			git_tree *a = NULL, *b = NULL;
			git_diff *diff = NULL;

			if (parents > 1)
				continue;
			check_lg2(git_commit_tree(&b, commit), "Get tree", NULL);
			if (parents == 1) {
				git_commit *parent;
				check_lg2(git_commit_parent(&parent, commit, 0), "Get parent", NULL);
				check_lg2(git_commit_tree(&a, parent), "Tree for parent", NULL);
				git_commit_free(parent);
			}

			check_lg2(git_diff_tree_to_tree(
				&diff, git_commit_owner(commit), a, b, &diffopts),
				"Diff commit with parent", NULL);
			check_lg2(
                git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, diff_output, NULL),
				"Displaying diff", NULL);

			git_diff_free(diff);
			git_tree_free(a);
			git_tree_free(b);
		}
	}

	git_pathspec_free(ps);
	git_revwalk_free(s.walker);
	git_repository_free(s.repo);
	git_threads_shutdown();

	return 0;
186 187
}

188
/** Determine if the given git_signature does not contain the filter text. */
189
static int signature_matches(const git_signature *sig, const char *filter) {
190
	if (filter == NULL)
191
		return 1;
192

193 194 195
	if (sig != NULL &&
		(strstr(sig->name, filter) != NULL ||
		strstr(sig->email, filter) != NULL))
196
		return 1;
Eoin Coffey committed
197 198 199 200

	return 0;
}

201
static int log_message_matches(const git_commit *commit, const char *filter) {
Eoin Coffey committed
202 203 204
	const char *message = NULL;

	if (filter == NULL)
205
		return 1;
Eoin Coffey committed
206

207 208
	if ((message = git_commit_message(commit)) != NULL &&
		strstr(message, filter) != NULL)
Eoin Coffey committed
209 210
		return 1;

211 212 213
	return 0;
}

214
/** Push object (for hide or show) onto revwalker. */
215 216 217 218
static void push_rev(struct log_state *s, git_object *obj, int hide)
{
	hide = s->hide ^ hide;

219
	/** Create revwalker on demand if it doesn't already exist. */
220
	if (!s->walker) {
221
		check_lg2(git_revwalk_new(&s->walker, s->repo),
222
			"Could not create revision walker", NULL);
223 224
		git_revwalk_sorting(s->walker, s->sorting);
	}
225 226

	if (!obj)
227
		check_lg2(git_revwalk_push_head(s->walker),
228 229
			"Could not find repository HEAD", NULL);
	else if (hide)
230
		check_lg2(git_revwalk_hide(s->walker, git_object_id(obj)),
231 232
			"Reference does not refer to a commit", NULL);
	else
233
		check_lg2(git_revwalk_push(s->walker, git_object_id(obj)),
234 235 236 237
			"Reference does not refer to a commit", NULL);

	git_object_free(obj);
}
238

239
/** Parse revision string and add revs to walker. */
240 241 242 243 244
static int add_revision(struct log_state *s, const char *revstr)
{
	git_revspec revs;
	int hide = 0;

245
	/** Open repo on demand if it isn't already open. */
246 247
	if (!s->repo) {
		if (!s->repodir) s->repodir = ".";
248
		check_lg2(git_repository_open_ext(&s->repo, s->repodir, 0, NULL),
249 250 251
			"Could not open repository", s->repodir);
	}

252
	if (!revstr) {
253
		push_rev(s, NULL, hide);
254 255 256 257
		return 0;
	}

	if (*revstr == '^') {
258 259
		revs.flags = GIT_REVPARSE_SINGLE;
		hide = !hide;
260 261

		if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0)
262
			return -1;
263 264
	} else if (git_revparse(&revs, s->repo, revstr) < 0)
		return -1;
265 266 267 268 269 270 271 272

	if ((revs.flags & GIT_REVPARSE_SINGLE) != 0)
		push_rev(s, revs.from, hide);
	else {
		push_rev(s, revs.to, hide);

		if ((revs.flags & GIT_REVPARSE_MERGE_BASE) != 0) {
			git_oid base;
273
			check_lg2(git_merge_base(&base, s->repo,
274 275
				git_object_id(revs.from), git_object_id(revs.to)),
				"Could not find merge base", revstr);
276 277
			check_lg2(
				git_object_lookup(&revs.to, s->repo, &base, GIT_OBJ_COMMIT),
278 279 280
				"Could not find merge base commit", NULL);

			push_rev(s, revs.to, hide);
281
		}
282 283

		push_rev(s, revs.from, !hide);
284 285
	}

286 287 288
	return 0;
}

289
/** Update revwalker with sorting mode. */
290 291
static void set_sorting(struct log_state *s, unsigned int sort_mode)
{
292
	/** Open repo on demand if it isn't already open. */
293 294 295 296 297 298
	if (!s->repo) {
		if (!s->repodir) s->repodir = ".";
		check_lg2(git_repository_open_ext(&s->repo, s->repodir, 0, NULL),
			"Could not open repository", s->repodir);
	}

299
	/** Create revwalker on demand if it doesn't already exist. */
300 301 302 303 304 305 306 307 308 309 310 311
	if (!s->walker)
		check_lg2(git_revwalk_new(&s->walker, s->repo),
			"Could not create revision walker", NULL);

	if (sort_mode == GIT_SORT_REVERSE)
		s->sorting = s->sorting ^ GIT_SORT_REVERSE;
	else
		s->sorting = sort_mode | (s->sorting & GIT_SORT_REVERSE);

	git_revwalk_sorting(s->walker, s->sorting);
}

312
/** Helper to format a git_time value like Git. */
313 314 315
static void print_time(const git_time *intime, const char *prefix)
{
	char sign, out[32];
316
	struct tm *intm;
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
	int offset, hours, minutes;
	time_t t;

	offset = intime->offset;
	if (offset < 0) {
		sign = '-';
		offset = -offset;
	} else {
		sign = '+';
	}

	hours   = offset / 60;
	minutes = offset % 60;

	t = (time_t)intime->time + (intime->offset * 60);

333 334
	intm = gmtime(&t);
	strftime(out, sizeof(out), "%a %b %e %T %Y", intm);
335 336 337 338

	printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes);
}

339
/** Helper to print a commit object. */
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
static void print_commit(git_commit *commit)
{
	char buf[GIT_OID_HEXSZ + 1];
	int i, count;
	const git_signature *sig;
	const char *scan, *eol;

	git_oid_tostr(buf, sizeof(buf), git_commit_id(commit));
	printf("commit %s\n", buf);

	if ((count = (int)git_commit_parentcount(commit)) > 1) {
		printf("Merge:");
		for (i = 0; i < count; ++i) {
			git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
			printf(" %s", buf);
		}
		printf("\n");
	}

	if ((sig = git_commit_author(commit)) != NULL) {
		printf("Author: %s <%s>\n", sig->name, sig->email);
		print_time(&sig->when, "Date:   ");
	}
	printf("\n");

	for (scan = git_commit_message(commit); scan && *scan; ) {
		for (eol = scan; *eol && *eol != '\n'; ++eol) /* find eol */;

		printf("    %.*s\n", (int)(eol - scan), scan);
		scan = *eol ? eol + 1 : NULL;
	}
	printf("\n");
}

374
/** Helper to find how many files in a commit changed from its nth parent. */
375
static int match_with_parent(git_commit *commit, int i, git_diff_options *opts)
376 377 378
{
	git_commit *parent;
	git_tree *a, *b;
379
	git_diff *diff;
380 381
	int ndeltas;

382 383 384 385 386 387 388
	check_lg2(
		git_commit_parent(&parent, commit, (size_t)i), "Get parent", NULL);
	check_lg2(git_commit_tree(&a, parent), "Tree for parent", NULL);
	check_lg2(git_commit_tree(&b, commit), "Tree for commit", NULL);
	check_lg2(
		git_diff_tree_to_tree(&diff, git_commit_owner(commit), a, b, opts),
		"Checking diff between parent and commit", NULL);
389 390 391

	ndeltas = (int)git_diff_num_deltas(diff);

392
	git_diff_free(diff);
393 394 395 396 397 398 399
	git_tree_free(a);
	git_tree_free(b);
	git_commit_free(parent);

	return ndeltas > 0;
}

400
/** Print a usage message for the program. */
401
static void usage(const char *message, const char *arg)
402
{
403 404 405 406 407 408 409
	if (message && arg)
		fprintf(stderr, "%s: %s\n", message, arg);
	else if (message)
		fprintf(stderr, "%s\n", message);
	fprintf(stderr, "usage: log [<options>]\n");
	exit(1);
}
410

411
/** Parse some log command line options. */
412 413 414 415
static int parse_options(
	struct log_state *s, struct log_options *opt, int argc, char **argv)
{
	struct args_info args = ARGS_INFO_INIT;
416

417 418
	memset(s, 0, sizeof(*s));
	s->sorting = GIT_SORT_TIME;
419

420 421 422
	memset(opt, 0, sizeof(*opt));
	opt->max_parents = -1;
	opt->limit = -1;
423

424 425
	for (args.pos = 1; args.pos < argc; ++args.pos) {
		const char *a = argv[args.pos];
426

427
		if (a[0] != '-') {
428 429
			if (!add_revision(s, a))
				s->revisions++;
430 431
			else
				/** Try failed revision parse as filename. */
432 433
				break;
		} else if (!strcmp(a, "--")) {
434
			++args.pos;
435
			break;
436
		}
437
		else if (!strcmp(a, "--date-order"))
438
			set_sorting(s, GIT_SORT_TIME);
439
		else if (!strcmp(a, "--topo-order"))
440
			set_sorting(s, GIT_SORT_TOPOLOGICAL);
441
		else if (!strcmp(a, "--reverse"))
442
			set_sorting(s, GIT_SORT_REVERSE);
443 444
		else if (match_str_arg(&opt->author, &args, "--author"))
			/** Found valid --author */;
445 446
		else if (match_str_arg(&opt->committer, &args, "--committer"))
			/** Found valid --committer */;
Eoin Coffey committed
447 448
		else if (match_str_arg(&opt->grep, &args, "--grep"))
			/** Found valid --grep */;
449
		else if (match_str_arg(&s->repodir, &args, "--git-dir"))
450
			/** Found git-dir. */;
451
		else if (match_int_arg(&opt->skip, &args, "--skip", 0))
452
			/** Found valid --skip. */;
453
		else if (match_int_arg(&opt->limit, &args, "--max-count", 0))
454
			/** Found valid --max-count. */;
455 456 457
		else if (a[1] >= '0' && a[1] <= '9')
			is_integer(&opt->limit, a + 1, 0);
		else if (match_int_arg(&opt->limit, &args, "-n", 0))
458
			/** Found valid -n. */;
459
		else if (!strcmp(a, "--merges"))
460
			opt->min_parents = 2;
461
		else if (!strcmp(a, "--no-merges"))
462
			opt->max_parents = 1;
463
		else if (!strcmp(a, "--no-min-parents"))
464
			opt->min_parents = 0;
465
		else if (!strcmp(a, "--no-max-parents"))
466 467
			opt->max_parents = -1;
		else if (match_int_arg(&opt->max_parents, &args, "--max-parents=", 1))
468
			/** Found valid --max-parents. */;
469
		else if (match_int_arg(&opt->min_parents, &args, "--min-parents=", 0))
470
			/** Found valid --min_parents. */;
471
		else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch"))
472
			opt->show_diff = 1;
473 474
		else
			usage("Unsupported argument", a);
475 476
	}

477
	return args.pos;
478
}
479