log.c 11.5 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 57 58 59 60
struct log_options {
	int show_diff;
	int skip, limit;
	int min_parents, max_parents;
	git_time_t before;
	git_time_t after;
	char *author;
	char *committer;
};

61
/** utility functions that parse options and help with log output */
62 63 64 65 66 67 68 69
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 *);


int main(int argc, char *argv[])
70
{
71 72 73 74 75 76 77
	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;
78

79
	git_threads_init();
80

81
	/** Parse arguments and set up revwalker. */
82

83 84 85 86 87 88 89 90 91 92 93
	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);

94
	/** Use the revwalker to traverse the history. */
95 96 97 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 134 135 136 137 138 139 140 141 142 143 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

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

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

175
/** Push object (for hide or show) onto revwalker. */
176 177 178 179
static void push_rev(struct log_state *s, git_object *obj, int hide)
{
	hide = s->hide ^ hide;

180
	/** Create revwalker on demand if it doesn't already exist. */
181
	if (!s->walker) {
182
		check_lg2(git_revwalk_new(&s->walker, s->repo),
183
			"Could not create revision walker", NULL);
184 185
		git_revwalk_sorting(s->walker, s->sorting);
	}
186 187

	if (!obj)
188
		check_lg2(git_revwalk_push_head(s->walker),
189 190
			"Could not find repository HEAD", NULL);
	else if (hide)
191
		check_lg2(git_revwalk_hide(s->walker, git_object_id(obj)),
192 193
			"Reference does not refer to a commit", NULL);
	else
194
		check_lg2(git_revwalk_push(s->walker, git_object_id(obj)),
195 196 197 198
			"Reference does not refer to a commit", NULL);

	git_object_free(obj);
}
199

200
/** Parse revision string and add revs to walker. */
201 202 203 204 205
static int add_revision(struct log_state *s, const char *revstr)
{
	git_revspec revs;
	int hide = 0;

206
	/** Open repo on demand if it isn't already open. */
207 208
	if (!s->repo) {
		if (!s->repodir) s->repodir = ".";
209
		check_lg2(git_repository_open_ext(&s->repo, s->repodir, 0, NULL),
210 211 212
			"Could not open repository", s->repodir);
	}

213
	if (!revstr) {
214
		push_rev(s, NULL, hide);
215 216 217 218
		return 0;
	}

	if (*revstr == '^') {
219 220
		revs.flags = GIT_REVPARSE_SINGLE;
		hide = !hide;
221 222

		if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0)
223
			return -1;
224 225
	} else if (git_revparse(&revs, s->repo, revstr) < 0)
		return -1;
226 227 228 229 230 231 232 233

	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;
234
			check_lg2(git_merge_base(&base, s->repo,
235 236
				git_object_id(revs.from), git_object_id(revs.to)),
				"Could not find merge base", revstr);
237 238
			check_lg2(
				git_object_lookup(&revs.to, s->repo, &base, GIT_OBJ_COMMIT),
239 240 241
				"Could not find merge base commit", NULL);

			push_rev(s, revs.to, hide);
242
		}
243 244

		push_rev(s, revs.from, !hide);
245 246
	}

247 248 249
	return 0;
}

250
/** Update revwalker with sorting mode. */
251 252
static void set_sorting(struct log_state *s, unsigned int sort_mode)
{
253
	/** Open repo on demand if it isn't already open. */
254 255 256 257 258 259
	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);
	}

260
	/** Create revwalker on demand if it doesn't already exist. */
261 262 263 264 265 266 267 268 269 270 271 272
	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);
}

273
/** Helper to format a git_time value like Git. */
274 275 276
static void print_time(const git_time *intime, const char *prefix)
{
	char sign, out[32];
277
	struct tm *intm;
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
	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);

294 295
	intm = gmtime(&t);
	strftime(out, sizeof(out), "%a %b %e %T %Y", intm);
296 297 298 299

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

300
/** Helper to print a commit object. */
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
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");
}

335
/** Helper to find how many files in a commit changed from its nth parent. */
336
static int match_with_parent(git_commit *commit, int i, git_diff_options *opts)
337 338 339
{
	git_commit *parent;
	git_tree *a, *b;
340
	git_diff *diff;
341 342
	int ndeltas;

343 344 345 346 347 348 349
	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);
350 351 352

	ndeltas = (int)git_diff_num_deltas(diff);

353
	git_diff_free(diff);
354 355 356 357 358 359 360
	git_tree_free(a);
	git_tree_free(b);
	git_commit_free(parent);

	return ndeltas > 0;
}

361
/** Print a usage message for the program. */
362
static void usage(const char *message, const char *arg)
363
{
364 365 366 367 368 369 370
	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);
}
371

372
/** Parse some log command line options. */
373 374 375 376
static int parse_options(
	struct log_state *s, struct log_options *opt, int argc, char **argv)
{
	struct args_info args = ARGS_INFO_INIT;
377

378 379
	memset(s, 0, sizeof(*s));
	s->sorting = GIT_SORT_TIME;
380

381 382 383
	memset(opt, 0, sizeof(*opt));
	opt->max_parents = -1;
	opt->limit = -1;
384

385 386
	for (args.pos = 1; args.pos < argc; ++args.pos) {
		const char *a = argv[args.pos];
387

388
		if (a[0] != '-') {
389 390
			if (!add_revision(s, a))
				s->revisions++;
391 392
			else
				/** Try failed revision parse as filename. */
393 394
				break;
		} else if (!strcmp(a, "--")) {
395
			++args.pos;
396
			break;
397
		}
398
		else if (!strcmp(a, "--date-order"))
399
			set_sorting(s, GIT_SORT_TIME);
400
		else if (!strcmp(a, "--topo-order"))
401
			set_sorting(s, GIT_SORT_TOPOLOGICAL);
402
		else if (!strcmp(a, "--reverse"))
403 404
			set_sorting(s, GIT_SORT_REVERSE);
		else if (match_str_arg(&s->repodir, &args, "--git-dir"))
405
			/** Found git-dir. */;
406
		else if (match_int_arg(&opt->skip, &args, "--skip", 0))
407
			/** Found valid --skip. */;
408
		else if (match_int_arg(&opt->limit, &args, "--max-count", 0))
409
			/** Found valid --max-count. */;
410 411 412
		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))
413
			/** Found valid -n. */;
414
		else if (!strcmp(a, "--merges"))
415
			opt->min_parents = 2;
416
		else if (!strcmp(a, "--no-merges"))
417
			opt->max_parents = 1;
418
		else if (!strcmp(a, "--no-min-parents"))
419
			opt->min_parents = 0;
420
		else if (!strcmp(a, "--no-max-parents"))
421 422
			opt->max_parents = -1;
		else if (match_int_arg(&opt->max_parents, &args, "--max-parents=", 1))
423
			/** Found valid --max-parents. */;
424
		else if (match_int_arg(&opt->min_parents, &args, "--min-parents=", 0))
425
			/** Found valid --min_parents. */;
426
		else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch"))
427
			opt->show_diff = 1;
428 429
		else
			usage("Unsupported argument", a);
430 431
	}

432
	return args.pos;
433
}
434