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
struct log_options {
52 53
	int show_diff;
	int show_log_size;
54 55 56 57
	int skip, limit;
	int min_parents, max_parents;
	git_time_t before;
	git_time_t after;
58
	const char *author;
59
	const char *committer;
Eoin Coffey committed
60
	const char *grep;
61 62
};

63
/** utility functions that parse options and help with log output */
64 65 66
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);
67
static void print_commit(git_commit *commit, struct log_options *opts);
68 69
static int match_with_parent(git_commit *commit, int i, git_diff_options *);

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

74
int lg2_log(git_repository *repo, int argc, char *argv[])
75
{
76 77 78 79 80 81 82
	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;
83

84
	/** Parse arguments and set up revwalker. */
85
	last_arg = parse_options(&s, &opt, argc, argv);
86
	s.repo = repo;
87 88 89 90 91 92 93 94 95 96

	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);

97
	/** Use the revwalker to traverse the history. */
98 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

	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;
		}

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

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

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

143 144 145 146 147 148 149
		if (count++ < opt.skip)
			continue;
		if (opt.limit != -1 && printed++ >= opt.limit) {
			git_commit_free(commit);
			break;
		}

150
		print_commit(commit, &opt);
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

		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);

	return 0;
183 184
}

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

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

	return 0;
}

198
static int log_message_matches(const git_commit *commit, const char *filter) {
Eoin Coffey committed
199 200 201
	const char *message = NULL;

	if (filter == NULL)
202
		return 1;
Eoin Coffey committed
203

204 205
	if ((message = git_commit_message(commit)) != NULL &&
		strstr(message, filter) != NULL)
Eoin Coffey committed
206 207
		return 1;

208 209 210
	return 0;
}

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

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

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

	git_object_free(obj);
}
235

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

242
	if (!revstr) {
243
		push_rev(s, NULL, hide);
244 245 246 247
		return 0;
	}

	if (*revstr == '^') {
248
		revs.flags = GIT_REVSPEC_SINGLE;
249
		hide = !hide;
250 251

		if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0)
252
			return -1;
253 254
	} else if (git_revparse(&revs, s->repo, revstr) < 0)
		return -1;
255

256
	if ((revs.flags & GIT_REVSPEC_SINGLE) != 0)
257 258 259 260
		push_rev(s, revs.from, hide);
	else {
		push_rev(s, revs.to, hide);

261
		if ((revs.flags & GIT_REVSPEC_MERGE_BASE) != 0) {
262
			git_oid base;
263
			check_lg2(git_merge_base(&base, s->repo,
264 265
				git_object_id(revs.from), git_object_id(revs.to)),
				"Could not find merge base", revstr);
266
			check_lg2(
267
				git_object_lookup(&revs.to, s->repo, &base, GIT_OBJECT_COMMIT),
268 269 270
				"Could not find merge base commit", NULL);

			push_rev(s, revs.to, hide);
271
		}
272 273

		push_rev(s, revs.from, !hide);
274 275
	}

276 277 278
	return 0;
}

279
/** Update revwalker with sorting mode. */
280 281
static void set_sorting(struct log_state *s, unsigned int sort_mode)
{
282
	/** Open repo on demand if it isn't already open. */
283 284 285 286 287 288
	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);
	}

289
	/** Create revwalker on demand if it doesn't already exist. */
290 291 292 293 294 295 296 297 298 299 300 301
	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);
}

302
/** Helper to format a git_time value like Git. */
303 304 305
static void print_time(const git_time *intime, const char *prefix)
{
	char sign, out[32];
306
	struct tm *intm;
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
	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);

323 324
	intm = gmtime(&t);
	strftime(out, sizeof(out), "%a %b %e %T %Y", intm);
325 326 327 328

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

329
/** Helper to print a commit object. */
330
static void print_commit(git_commit *commit, struct log_options *opts)
331
{
332
	char buf[GIT_OID_SHA1_HEXSIZE + 1];
333 334 335 336 337 338 339
	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);

340 341
	if (opts->show_log_size) {
		printf("log size %d\n", (int)strlen(git_commit_message(commit)));
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
	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");
}

368
/** Helper to find how many files in a commit changed from its nth parent. */
369
static int match_with_parent(git_commit *commit, int i, git_diff_options *opts)
370 371 372
{
	git_commit *parent;
	git_tree *a, *b;
373
	git_diff *diff;
374 375
	int ndeltas;

376 377 378 379 380 381 382
	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);
383 384 385

	ndeltas = (int)git_diff_num_deltas(diff);

386
	git_diff_free(diff);
387 388 389 390 391 392 393
	git_tree_free(a);
	git_tree_free(b);
	git_commit_free(parent);

	return ndeltas > 0;
}

394
/** Print a usage message for the program. */
395
static void usage(const char *message, const char *arg)
396
{
397 398 399 400 401 402 403
	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);
}
404

405
/** Parse some log command line options. */
406 407 408 409
static int parse_options(
	struct log_state *s, struct log_options *opt, int argc, char **argv)
{
	struct args_info args = ARGS_INFO_INIT;
410

411 412
	memset(s, 0, sizeof(*s));
	s->sorting = GIT_SORT_TIME;
413

414 415 416
	memset(opt, 0, sizeof(*opt));
	opt->max_parents = -1;
	opt->limit = -1;
417

418 419
	for (args.pos = 1; args.pos < argc; ++args.pos) {
		const char *a = argv[args.pos];
420

421
		if (a[0] != '-') {
422 423
			if (!add_revision(s, a))
				s->revisions++;
424 425
			else
				/** Try failed revision parse as filename. */
426
				break;
427
		} else if (!match_arg_separator(&args)) {
428
			break;
429
		}
430
		else if (!strcmp(a, "--date-order"))
431
			set_sorting(s, GIT_SORT_TIME);
432
		else if (!strcmp(a, "--topo-order"))
433
			set_sorting(s, GIT_SORT_TOPOLOGICAL);
434
		else if (!strcmp(a, "--reverse"))
435
			set_sorting(s, GIT_SORT_REVERSE);
436
		else if (match_str_arg(&opt->author, &args, "--author"))
437 438
			/** Found valid --author */
			;
439
		else if (match_str_arg(&opt->committer, &args, "--committer"))
440 441
			/** Found valid --committer */
			;
Eoin Coffey committed
442
		else if (match_str_arg(&opt->grep, &args, "--grep"))
443 444
			/** Found valid --grep */
			;
445
		else if (match_str_arg(&s->repodir, &args, "--git-dir"))
446 447
			/** Found git-dir. */
			;
448
		else if (match_int_arg(&opt->skip, &args, "--skip", 0))
449 450
			/** Found valid --skip. */
			;
451
		else if (match_int_arg(&opt->limit, &args, "--max-count", 0))
452 453
			/** Found valid --max-count. */
			;
454 455 456
		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))
457 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 469
			/** Found valid --max-parents. */
			;
470
		else if (match_int_arg(&opt->min_parents, &args, "--min-parents=", 0))
471 472
			/** Found valid --min_parents. */
			;
473
		else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch"))
474
			opt->show_diff = 1;
475 476
		else if (!strcmp(a, "--log-size"))
			opt->show_log_size = 1;
477 478
		else
			usage("Unsupported argument", a);
479 480
	}

481
	return args.pos;
482
}
483