]>
Commit | Line | Data |
---|---|---|
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 | 36 | struct 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 |
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 | ||
85c6730c | 50 | /** log_options holds other command line options that affect log output */ |
66902d47 RB |
51 | struct 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 |
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); | |
eae0bfdc | 67 | static void print_commit(git_commit *commit, struct log_options *opts); |
66902d47 RB |
68 | static int match_with_parent(git_commit *commit, int i, git_diff_options *); |
69 | ||
26cce321 | 70 | /** utility functions for filtering */ |
33bf1b1a EC |
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); | |
66902d47 | 73 | |
22a2d3d5 | 74 | int 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 | 186 | static 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 | 198 | static 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 |
212 | static 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 |
237 | static 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 |
280 | static 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 |
303 | static 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 | 330 | static 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 | 369 | static 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 | 395 | static 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 |
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; | |
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 |