]> git.proxmox.com Git - libgit2.git/blob - examples/log.c
Library transition to 1.0.0
[libgit2.git] / examples / log.c
1 /*
2 * libgit2 "log" example - shows how to walk history and get commit info
3 *
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/>.
13 */
14
15 #include "common.h"
16
17 /**
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:
23 *
24 * - Robust error handling
25 * - Colorized or paginated output formatting
26 * - Most of the `git log` options
27 *
28 * This does have:
29 *
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
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 int hide;
41 int sorting;
42 int revisions;
43 };
44
45 /** utility functions that are called to configure the walker */
46 static void set_sorting(struct log_state *s, unsigned int sort_mode);
47 static void push_rev(struct log_state *s, git_object *obj, int hide);
48 static int add_revision(struct log_state *s, const char *revstr);
49
50 /** log_options holds other command line options that affect log output */
51 struct log_options {
52 int show_diff;
53 int show_log_size;
54 int skip, limit;
55 int min_parents, max_parents;
56 git_time_t before;
57 git_time_t after;
58 const char *author;
59 const char *committer;
60 const char *grep;
61 };
62
63 /** utility functions that parse options and help with log output */
64 static int parse_options(
65 struct log_state *s, struct log_options *opt, int argc, char **argv);
66 static void print_time(const git_time *intime, const char *prefix);
67 static void print_commit(git_commit *commit, struct log_options *opts);
68 static int match_with_parent(git_commit *commit, int i, git_diff_options *);
69
70 /** utility functions for filtering */
71 static int signature_matches(const git_signature *sig, const char *filter);
72 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 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;
83
84 /** Parse arguments and set up revwalker. */
85 last_arg = parse_options(&s, &opt, argc, argv);
86 s.repo = repo;
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
97 /** Use the revwalker to traverse the history. */
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
134 if (!signature_matches(git_commit_author(commit), opt.author))
135 continue;
136
137 if (!signature_matches(git_commit_committer(commit), opt.committer))
138 continue;
139
140 if (!log_message_matches(commit, opt.grep))
141 continue;
142
143 if (count++ < opt.skip)
144 continue;
145 if (opt.limit != -1 && printed++ >= opt.limit) {
146 git_commit_free(commit);
147 break;
148 }
149
150 print_commit(commit, &opt);
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);
181
182 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 if (sig != NULL &&
191 (strstr(sig->name, filter) != NULL ||
192 strstr(sig->email, filter) != NULL))
193 return 1;
194
195 return 0;
196 }
197
198 static int log_message_matches(const git_commit *commit, const char *filter) {
199 const char *message = NULL;
200
201 if (filter == NULL)
202 return 1;
203
204 if ((message = git_commit_message(commit)) != NULL &&
205 strstr(message, filter) != NULL)
206 return 1;
207
208 return 0;
209 }
210
211 /** Push object (for hide or show) onto revwalker. */
212 static void push_rev(struct log_state *s, git_object *obj, int hide)
213 {
214 hide = s->hide ^ hide;
215
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 git_revwalk_sorting(s->walker, s->sorting);
221 }
222
223 if (!obj)
224 check_lg2(git_revwalk_push_head(s->walker),
225 "Could not find repository HEAD", NULL);
226 else if (hide)
227 check_lg2(git_revwalk_hide(s->walker, git_object_id(obj)),
228 "Reference does not refer to a commit", NULL);
229 else
230 check_lg2(git_revwalk_push(s->walker, git_object_id(obj)),
231 "Reference does not refer to a commit", NULL);
232
233 git_object_free(obj);
234 }
235
236 /** Parse revision string and add revs to walker. */
237 static int add_revision(struct log_state *s, const char *revstr)
238 {
239 git_revspec revs;
240 int hide = 0;
241
242 if (!revstr) {
243 push_rev(s, NULL, hide);
244 return 0;
245 }
246
247 if (*revstr == '^') {
248 revs.flags = GIT_REVPARSE_SINGLE;
249 hide = !hide;
250
251 if (git_revparse_single(&revs.from, s->repo, revstr + 1) < 0)
252 return -1;
253 } else if (git_revparse(&revs, s->repo, revstr) < 0)
254 return -1;
255
256 if ((revs.flags & GIT_REVPARSE_SINGLE) != 0)
257 push_rev(s, revs.from, hide);
258 else {
259 push_rev(s, revs.to, hide);
260
261 if ((revs.flags & GIT_REVPARSE_MERGE_BASE) != 0) {
262 git_oid base;
263 check_lg2(git_merge_base(&base, s->repo,
264 git_object_id(revs.from), git_object_id(revs.to)),
265 "Could not find merge base", revstr);
266 check_lg2(
267 git_object_lookup(&revs.to, s->repo, &base, GIT_OBJECT_COMMIT),
268 "Could not find merge base commit", NULL);
269
270 push_rev(s, revs.to, hide);
271 }
272
273 push_rev(s, revs.from, !hide);
274 }
275
276 return 0;
277 }
278
279 /** Update revwalker with sorting mode. */
280 static void set_sorting(struct log_state *s, unsigned int sort_mode)
281 {
282 /** Open repo on demand if it isn't already open. */
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
289 /** Create revwalker on demand if it doesn't already exist. */
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
302 /** Helper to format a git_time value like Git. */
303 static void print_time(const git_time *intime, const char *prefix)
304 {
305 char sign, out[32];
306 struct tm *intm;
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
323 intm = gmtime(&t);
324 strftime(out, sizeof(out), "%a %b %e %T %Y", intm);
325
326 printf("%s%s %c%02d%02d\n", prefix, out, sign, hours, minutes);
327 }
328
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_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
340 if (opts->show_log_size) {
341 printf("log size %d\n", (int)strlen(git_commit_message(commit)));
342 }
343
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
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 git_commit *parent;
372 git_tree *a, *b;
373 git_diff *diff;
374 int ndeltas;
375
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);
383
384 ndeltas = (int)git_diff_num_deltas(diff);
385
386 git_diff_free(diff);
387 git_tree_free(a);
388 git_tree_free(b);
389 git_commit_free(parent);
390
391 return ndeltas > 0;
392 }
393
394 /** Print a usage message for the program. */
395 static void usage(const char *message, const char *arg)
396 {
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 }
404
405 /** Parse some log command line options. */
406 static 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;
410
411 memset(s, 0, sizeof(*s));
412 s->sorting = GIT_SORT_TIME;
413
414 memset(opt, 0, sizeof(*opt));
415 opt->max_parents = -1;
416 opt->limit = -1;
417
418 for (args.pos = 1; args.pos < argc; ++args.pos) {
419 const char *a = argv[args.pos];
420
421 if (a[0] != '-') {
422 if (!add_revision(s, a))
423 s->revisions++;
424 else
425 /** 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 /** Found valid --author */;
438 else if (match_str_arg(&opt->committer, &args, "--committer"))
439 /** Found valid --committer */;
440 else if (match_str_arg(&opt->grep, &args, "--grep"))
441 /** Found valid --grep */;
442 else if (match_str_arg(&s->repodir, &args, "--git-dir"))
443 /** Found git-dir. */;
444 else if (match_int_arg(&opt->skip, &args, "--skip", 0))
445 /** Found valid --skip. */;
446 else if (match_int_arg(&opt->limit, &args, "--max-count", 0))
447 /** Found valid --max-count. */;
448 else if (a[1] >= '0' && a[1] <= '9')
449 is_integer(&opt->limit, a + 1, 0);
450 else if (match_int_arg(&opt->limit, &args, "-n", 0))
451 /** Found valid -n. */;
452 else if (!strcmp(a, "--merges"))
453 opt->min_parents = 2;
454 else if (!strcmp(a, "--no-merges"))
455 opt->max_parents = 1;
456 else if (!strcmp(a, "--no-min-parents"))
457 opt->min_parents = 0;
458 else if (!strcmp(a, "--no-max-parents"))
459 opt->max_parents = -1;
460 else if (match_int_arg(&opt->max_parents, &args, "--max-parents=", 1))
461 /** Found valid --max-parents. */;
462 else if (match_int_arg(&opt->min_parents, &args, "--min-parents=", 0))
463 /** Found valid --min_parents. */;
464 else if (!strcmp(a, "-p") || !strcmp(a, "-u") || !strcmp(a, "--patch"))
465 opt->show_diff = 1;
466 else if (!strcmp(a, "--log-size"))
467 opt->show_log_size = 1;
468 else
469 usage("Unsupported argument", a);
470 }
471
472 return args.pos;
473 }
474