]> git.proxmox.com Git - libgit2.git/blame - examples/log.c
New upstream version 1.5.0+ds
[libgit2.git] / examples / log.c
CommitLineData
66902d47 1/*
6cb831bd 2 * libgit2 "log" example - shows how to walk history and get commit info
66902d47 3 *
6cb831bd
BS
4 * Written by the libgit2 contributors
5 *
6 * To the extent possible under law, the author(s) have dedicated all copyright
7 * and related and neighboring rights to this software to the public domain
8 * worldwide. This software is distributed without any warranty.
9 *
10 * You should have received a copy of the CC0 Public Domain Dedication along
11 * with this software. If not, see
12 * <http://creativecommons.org/publicdomain/zero/1.0/>.
66902d47
RB
13 */
14
15#include "common.h"
16
85c6730c 17/**
66902d47
RB
18 * This example demonstrates the libgit2 rev walker APIs to roughly
19 * simulate the output of `git log` and a few of command line arguments.
20 * `git log` has many many options and this only shows a few of them.
21 *
22 * This does not have:
85c6730c 23 *
66902d47
RB
24 * - Robust error handling
25 * - Colorized or paginated output formatting
26 * - Most of the `git log` options
27 *
28 * This does have:
85c6730c 29 *
66902d47
RB
30 * - Examples of translating command line arguments to equivalent libgit2
31 * revwalker configuration calls
32 * - Simplified options to apply pathspec limits and to show basic diffs
33 */
34
85c6730c 35/** log_state represents walker being configured while handling options */
d0628e2f 36struct log_state {
d39fff36 37 git_repository *repo;
d0628e2f 38 const char *repodir;
0d44d3dc 39 git_revwalk *walker;
d0628e2f
RB
40 int hide;
41 int sorting;
66902d47 42 int revisions;
d0628e2f 43};
d39fff36 44
85c6730c 45/** utility functions that are called to configure the walker */
66902d47
RB
46static void set_sorting(struct log_state *s, unsigned int sort_mode);
47static void push_rev(struct log_state *s, git_object *obj, int hide);
48static int add_revision(struct log_state *s, const char *revstr);
49
85c6730c 50/** log_options holds other command line options that affect log output */
66902d47
RB
51struct log_options {
52 int show_diff;
eae0bfdc 53 int show_log_size;
66902d47
RB
54 int skip, limit;
55 int min_parents, max_parents;
56 git_time_t before;
57 git_time_t after;
97fc71ab 58 const char *author;
161e6dc1 59 const char *committer;
26cce321 60 const char *grep;
66902d47
RB
61};
62
85c6730c 63/** utility functions that parse options and help with log output */
66902d47
RB
64static int parse_options(
65 struct log_state *s, struct log_options *opt, int argc, char **argv);
66static void print_time(const git_time *intime, const char *prefix);
eae0bfdc 67static void print_commit(git_commit *commit, struct log_options *opts);
66902d47
RB
68static int match_with_parent(git_commit *commit, int i, git_diff_options *);
69
26cce321 70/** utility functions for filtering */
33bf1b1a
EC
71static int signature_matches(const git_signature *sig, const char *filter);
72static int log_message_matches(const git_commit *commit, const char *filter);
66902d47 73
22a2d3d5 74int lg2_log(git_repository *repo, int argc, char *argv[])
d0628e2f 75{
66902d47
RB
76 int i, count = 0, printed = 0, parents, last_arg;
77 struct log_state s;
78 struct log_options opt;
79 git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT;
80 git_oid oid;
81 git_commit *commit = NULL;
82 git_pathspec *ps = NULL;
d39fff36 83
85c6730c 84 /** Parse arguments and set up revwalker. */
66902d47 85 last_arg = parse_options(&s, &opt, argc, argv);
22a2d3d5 86 s.repo = repo;
66902d47
RB
87
88 diffopts.pathspec.strings = &argv[last_arg];
89 diffopts.pathspec.count = argc - last_arg;
90 if (diffopts.pathspec.count > 0)
91 check_lg2(git_pathspec_new(&ps, &diffopts.pathspec),
92 "Building pathspec", NULL);
93
94 if (!s.revisions)
95 add_revision(&s, NULL);
96
85c6730c 97 /** Use the revwalker to traverse the history. */
66902d47
RB
98
99 printed = count = 0;
100
101 for (; !git_revwalk_next(&oid, s.walker); git_commit_free(commit)) {
102 check_lg2(git_commit_lookup(&commit, s.repo, &oid),
103 "Failed to look up commit", NULL);
104
105 parents = (int)git_commit_parentcount(commit);
106 if (parents < opt.min_parents)
107 continue;
108 if (opt.max_parents > 0 && parents > opt.max_parents)
109 continue;
110
111 if (diffopts.pathspec.count > 0) {
112 int unmatched = parents;
113
114 if (parents == 0) {
115 git_tree *tree;
116 check_lg2(git_commit_tree(&tree, commit), "Get tree", NULL);
117 if (git_pathspec_match_tree(
118 NULL, tree, GIT_PATHSPEC_NO_MATCH_ERROR, ps) != 0)
119 unmatched = 1;
120 git_tree_free(tree);
121 } else if (parents == 1) {
122 unmatched = match_with_parent(commit, 0, &diffopts) ? 0 : 1;
123 } else {
124 for (i = 0; i < parents; ++i) {
125 if (match_with_parent(commit, i, &diffopts))
126 unmatched--;
127 }
128 }
129
130 if (unmatched > 0)
131 continue;
132 }
133
33bf1b1a 134 if (!signature_matches(git_commit_author(commit), opt.author))
161e6dc1
EC
135 continue;
136
33bf1b1a 137 if (!signature_matches(git_commit_committer(commit), opt.committer))
161e6dc1 138 continue;
97fc71ab 139
33bf1b1a 140 if (!log_message_matches(commit, opt.grep))
26cce321
EC
141 continue;
142
66902d47
RB
143 if (count++ < opt.skip)
144 continue;
145 if (opt.limit != -1 && printed++ >= opt.limit) {
146 git_commit_free(commit);
147 break;
148 }
149
eae0bfdc 150 print_commit(commit, &opt);
66902d47
RB
151
152 if (opt.show_diff) {
153 git_tree *a = NULL, *b = NULL;
154 git_diff *diff = NULL;
155
156 if (parents > 1)
157 continue;
158 check_lg2(git_commit_tree(&b, commit), "Get tree", NULL);
159 if (parents == 1) {
160 git_commit *parent;
161 check_lg2(git_commit_parent(&parent, commit, 0), "Get parent", NULL);
162 check_lg2(git_commit_tree(&a, parent), "Tree for parent", NULL);
163 git_commit_free(parent);
164 }
165
166 check_lg2(git_diff_tree_to_tree(
167 &diff, git_commit_owner(commit), a, b, &diffopts),
168 "Diff commit with parent", NULL);
169 check_lg2(
170 git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, diff_output, NULL),
171 "Displaying diff", NULL);
172
173 git_diff_free(diff);
174 git_tree_free(a);
175 git_tree_free(b);
176 }
177 }
178
179 git_pathspec_free(ps);
180 git_revwalk_free(s.walker);
66902d47
RB
181
182 return 0;
d0628e2f
RB
183}
184
161e6dc1 185/** Determine if the given git_signature does not contain the filter text. */
33bf1b1a 186static int signature_matches(const git_signature *sig, const char *filter) {
161e6dc1 187 if (filter == NULL)
33bf1b1a 188 return 1;
161e6dc1 189
33bf1b1a
EC
190 if (sig != NULL &&
191 (strstr(sig->name, filter) != NULL ||
192 strstr(sig->email, filter) != NULL))
161e6dc1 193 return 1;
26cce321
EC
194
195 return 0;
196}
197
33bf1b1a 198static int log_message_matches(const git_commit *commit, const char *filter) {
26cce321
EC
199 const char *message = NULL;
200
201 if (filter == NULL)
33bf1b1a 202 return 1;
26cce321 203
33bf1b1a
EC
204 if ((message = git_commit_message(commit)) != NULL &&
205 strstr(message, filter) != NULL)
26cce321
EC
206 return 1;
207
161e6dc1
EC
208 return 0;
209}
210
85c6730c 211/** Push object (for hide or show) onto revwalker. */
d0628e2f
RB
212static void push_rev(struct log_state *s, git_object *obj, int hide)
213{
214 hide = s->hide ^ hide;
215
85c6730c 216 /** Create revwalker on demand if it doesn't already exist. */
2b3bd8ec 217 if (!s->walker) {
66902d47 218 check_lg2(git_revwalk_new(&s->walker, s->repo),
d0628e2f 219 "Could not create revision walker", NULL);
2b3bd8ec
RB
220 git_revwalk_sorting(s->walker, s->sorting);
221 }
d0628e2f
RB
222
223 if (!obj)
66902d47 224 check_lg2(git_revwalk_push_head(s->walker),
d0628e2f
RB
225 "Could not find repository HEAD", NULL);
226 else if (hide)
66902d47 227 check_lg2(git_revwalk_hide(s->walker, git_object_id(obj)),
d0628e2f
RB
228 "Reference does not refer to a commit", NULL);
229 else
66902d47 230 check_lg2(git_revwalk_push(s->walker, git_object_id(obj)),
d0628e2f
RB
231 "Reference does not refer to a commit", NULL);
232
233 git_object_free(obj);
234}
d39fff36 235
85c6730c 236/** Parse revision string and add revs to walker. */
d0628e2f
RB
237static int add_revision(struct log_state *s, const char *revstr)
238{
239 git_revspec revs;
240 int hide = 0;
241
5a169711 242 if (!revstr) {
d0628e2f 243 push_rev(s, NULL, hide);
5a169711
RB
244 return 0;
245 }
246
247 if (*revstr == '^') {
c25aa7cd 248 revs.flags = GIT_REVSPEC_SINGLE;
d0628e2f 249 hide = !hide;
733c4f3a
RB
250
251 if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0)
d0628e2f 252 return -1;
733c4f3a
RB
253 } else if (git_revparse(&revs, s->repo, revstr) < 0)
254 return -1;
d0628e2f 255
c25aa7cd 256 if ((revs.flags & GIT_REVSPEC_SINGLE) != 0)
d0628e2f
RB
257 push_rev(s, revs.from, hide);
258 else {
259 push_rev(s, revs.to, hide);
260
c25aa7cd 261 if ((revs.flags & GIT_REVSPEC_MERGE_BASE) != 0) {
d0628e2f 262 git_oid base;
66902d47 263 check_lg2(git_merge_base(&base, s->repo,
d0628e2f
RB
264 git_object_id(revs.from), git_object_id(revs.to)),
265 "Could not find merge base", revstr);
66902d47 266 check_lg2(
ac3d33df 267 git_object_lookup(&revs.to, s->repo, &base, GIT_OBJECT_COMMIT),
d0628e2f
RB
268 "Could not find merge base commit", NULL);
269
270 push_rev(s, revs.to, hide);
d39fff36 271 }
d0628e2f
RB
272
273 push_rev(s, revs.from, !hide);
d39fff36
RB
274 }
275
d0628e2f
RB
276 return 0;
277}
278
85c6730c 279/** Update revwalker with sorting mode. */
66902d47
RB
280static void set_sorting(struct log_state *s, unsigned int sort_mode)
281{
85c6730c 282 /** Open repo on demand if it isn't already open. */
66902d47
RB
283 if (!s->repo) {
284 if (!s->repodir) s->repodir = ".";
285 check_lg2(git_repository_open_ext(&s->repo, s->repodir, 0, NULL),
286 "Could not open repository", s->repodir);
287 }
288
85c6730c 289 /** Create revwalker on demand if it doesn't already exist. */
66902d47
RB
290 if (!s->walker)
291 check_lg2(git_revwalk_new(&s->walker, s->repo),
292 "Could not create revision walker", NULL);
293
294 if (sort_mode == GIT_SORT_REVERSE)
295 s->sorting = s->sorting ^ GIT_SORT_REVERSE;
296 else
297 s->sorting = sort_mode | (s->sorting & GIT_SORT_REVERSE);
298
299 git_revwalk_sorting(s->walker, s->sorting);
300}
301
85c6730c 302/** Helper to format a git_time value like Git. */
f44c4fa1
RB
303static void print_time(const git_time *intime, const char *prefix)
304{
305 char sign, out[32];
864e7271 306 struct tm *intm;
f44c4fa1
RB
307 int offset, hours, minutes;
308 time_t t;
309
310 offset = intime->offset;
311 if (offset < 0) {
312 sign = '-';
313 offset = -offset;
314 } else {
315 sign = '+';
316 }
317
318 hours = offset / 60;
319 minutes = offset % 60;
320
321 t = (time_t)intime->time + (intime->offset * 60);
322
864e7271
L
323 intm = gmtime(&t);
324 strftime(out, sizeof(out), "%a %b %e %T %Y", intm);
f44c4fa1
RB
325
326 printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes);
327}
328
85c6730c 329/** Helper to print a commit object. */
eae0bfdc 330static void print_commit(git_commit *commit, struct log_options *opts)
a8b5f116
RB
331{
332 char buf[GIT_OID_HEXSZ + 1];
333 int i, count;
334 const git_signature *sig;
335 const char *scan, *eol;
336
337 git_oid_tostr(buf, sizeof(buf), git_commit_id(commit));
338 printf("commit %s\n", buf);
339
eae0bfdc
PP
340 if (opts->show_log_size) {
341 printf("log size %d\n", (int)strlen(git_commit_message(commit)));
342 }
343
a8b5f116
RB
344 if ((count = (int)git_commit_parentcount(commit)) > 1) {
345 printf("Merge:");
346 for (i = 0; i < count; ++i) {
347 git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
348 printf(" %s", buf);
349 }
350 printf("\n");
351 }
352
353 if ((sig = git_commit_author(commit)) != NULL) {
354 printf("Author: %s <%s>\n", sig->name, sig->email);
355 print_time(&sig->when, "Date: ");
356 }
357 printf("\n");
358
359 for (scan = git_commit_message(commit); scan && *scan; ) {
360 for (eol = scan; *eol && *eol != '\n'; ++eol) /* find eol */;
361
362 printf(" %.*s\n", (int)(eol - scan), scan);
363 scan = *eol ? eol + 1 : NULL;
364 }
365 printf("\n");
366}
367
85c6730c 368/** Helper to find how many files in a commit changed from its nth parent. */
66902d47 369static int match_with_parent(git_commit *commit, int i, git_diff_options *opts)
a8b5f116
RB
370{
371 git_commit *parent;
372 git_tree *a, *b;
3ff1d123 373 git_diff *diff;
a8b5f116
RB
374 int ndeltas;
375
66902d47
RB
376 check_lg2(
377 git_commit_parent(&parent, commit, (size_t)i), "Get parent", NULL);
378 check_lg2(git_commit_tree(&a, parent), "Tree for parent", NULL);
379 check_lg2(git_commit_tree(&b, commit), "Tree for commit", NULL);
380 check_lg2(
381 git_diff_tree_to_tree(&diff, git_commit_owner(commit), a, b, opts),
382 "Checking diff between parent and commit", NULL);
a8b5f116
RB
383
384 ndeltas = (int)git_diff_num_deltas(diff);
385
3ff1d123 386 git_diff_free(diff);
a8b5f116
RB
387 git_tree_free(a);
388 git_tree_free(b);
389 git_commit_free(parent);
390
391 return ndeltas > 0;
392}
393
85c6730c 394/** Print a usage message for the program. */
66902d47 395static void usage(const char *message, const char *arg)
d0628e2f 396{
66902d47
RB
397 if (message && arg)
398 fprintf(stderr, "%s: %s\n", message, arg);
399 else if (message)
400 fprintf(stderr, "%s\n", message);
401 fprintf(stderr, "usage: log [<options>]\n");
402 exit(1);
403}
0d44d3dc 404
85c6730c 405/** Parse some log command line options. */
66902d47
RB
406static int parse_options(
407 struct log_state *s, struct log_options *opt, int argc, char **argv)
408{
409 struct args_info args = ARGS_INFO_INIT;
0d44d3dc 410
66902d47
RB
411 memset(s, 0, sizeof(*s));
412 s->sorting = GIT_SORT_TIME;
0d44d3dc 413
66902d47
RB
414 memset(opt, 0, sizeof(*opt));
415 opt->max_parents = -1;
416 opt->limit = -1;
bc6f0839 417
66902d47
RB
418 for (args.pos = 1; args.pos < argc; ++args.pos) {
419 const char *a = argv[args.pos];
0d44d3dc 420
d0628e2f 421 if (a[0] != '-') {
66902d47
RB
422 if (!add_revision(s, a))
423 s->revisions++;
85c6730c
BS
424 else
425 /** Try failed revision parse as filename. */
d0628e2f 426 break;
22a2d3d5 427 } else if (!match_arg_separator(&args)) {
d0628e2f 428 break;
0d44d3dc 429 }
d0628e2f 430 else if (!strcmp(a, "--date-order"))
66902d47 431 set_sorting(s, GIT_SORT_TIME);
d0628e2f 432 else if (!strcmp(a, "--topo-order"))
66902d47 433 set_sorting(s, GIT_SORT_TOPOLOGICAL);
d0628e2f 434 else if (!strcmp(a, "--reverse"))
66902d47 435 set_sorting(s, GIT_SORT_REVERSE);
97fc71ab 436 else if (match_str_arg(&opt->author, &args, "--author"))
22a2d3d5
UG
437 /** Found valid --author */
438 ;
161e6dc1 439 else if (match_str_arg(&opt->committer, &args, "--committer"))
22a2d3d5
UG
440 /** Found valid --committer */
441 ;
26cce321 442 else if (match_str_arg(&opt->grep, &args, "--grep"))
22a2d3d5
UG
443 /** Found valid --grep */
444 ;
66902d47 445 else if (match_str_arg(&s->repodir, &args, "--git-dir"))
22a2d3d5
UG
446 /** Found git-dir. */
447 ;
66902d47 448 else if (match_int_arg(&opt->skip, &args, "--skip", 0))
22a2d3d5
UG
449 /** Found valid --skip. */
450 ;
66902d47 451 else if (match_int_arg(&opt->limit, &args, "--max-count", 0))
22a2d3d5
UG
452 /** Found valid --max-count. */
453 ;
66902d47
RB
454 else if (a[1] >= '0' && a[1] <= '9')
455 is_integer(&opt->limit, a + 1, 0);
456 else if (match_int_arg(&opt->limit, &args, "-n", 0))
22a2d3d5
UG
457 /** Found valid -n. */
458 ;
bc6f0839 459 else if (!strcmp(a, "--merges"))
66902d47 460 opt->min_parents = 2;
bc6f0839 461 else if (!strcmp(a, "--no-merges"))
66902d47 462 opt->max_parents = 1;
bc6f0839 463 else if (!strcmp(a, "--no-min-parents"))
66902d47 464 opt->min_parents = 0;
bc6f0839 465 else if (!strcmp(a, "--no-max-parents"))
66902d47
RB
466 opt->max_parents = -1;
467 else if (match_int_arg(&opt->max_parents, &args, "--max-parents=", 1))
22a2d3d5
UG
468 /** Found valid --max-parents. */
469 ;
66902d47 470 else if (match_int_arg(&opt->min_parents, &args, "--min-parents=", 0))
22a2d3d5
UG
471 /** Found valid --min_parents. */
472 ;
bc6f0839 473 else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch"))
66902d47 474 opt->show_diff = 1;
eae0bfdc
PP
475 else if (!strcmp(a, "--log-size"))
476 opt->show_log_size = 1;
d0628e2f
RB
477 else
478 usage("Unsupported argument", a);
0d44d3dc
RB
479 }
480
66902d47 481 return args.pos;
d39fff36 482}
66902d47 483