1 /* 2 * libgit2 "blame" example - shows how to use the blame API 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 * <https://creativecommons.org/publicdomain/zero/1.0/>. 13 */ 14 /** 15 * This example demonstrates how to invoke the libgit2 blame API to roughly 16 * simulate the output of `git blame` and a few of its command line arguments. 17 * 18 * License: $(LINK2 https://creativecommons.org/publicdomain/zero/1.0/, CC0 1.0 Universal) 19 */ 20 module libgit2.example.blame; 21 22 23 private static import core.stdc.stdio; 24 private static import core.stdc.stdlib; 25 private static import core.stdc.string; 26 private static import libgit2.blame; 27 private static import libgit2.blob; 28 private static import libgit2.example.common; 29 private static import libgit2.object; 30 private static import libgit2.oid; 31 private static import libgit2.revparse; 32 private static import libgit2.types; 33 private static import std.ascii; 34 35 extern (C) 36 public struct blame_opts 37 { 38 char* path; 39 char* commitspec; 40 int C; 41 int M; 42 int start_line; 43 int end_line; 44 int F; 45 } 46 47 extern (C) 48 nothrow @nogc 49 public int lg2_blame(libgit2.types.git_repository* repo, int argc, char** argv) 50 51 in 52 { 53 } 54 55 do 56 { 57 .blame_opts o = .blame_opts.init; 58 .parse_opts(&o, argc, argv); 59 60 libgit2.blame.git_blame_options blameopts = libgit2.blame.GIT_BLAME_OPTIONS_INIT(); 61 62 if (o.M) { 63 blameopts.flags |= libgit2.blame.git_blame_flag_t.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES; 64 } 65 66 if (o.C) { 67 blameopts.flags |= libgit2.blame.git_blame_flag_t.GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES; 68 } 69 70 if (o.F) { 71 blameopts.flags |= libgit2.blame.git_blame_flag_t.GIT_BLAME_FIRST_PARENT; 72 } 73 74 libgit2.revparse.git_revspec revspec = libgit2.revparse.git_revspec.init; 75 76 /** 77 * The commit range comes in "committish" form. Use the rev-parse API to 78 * nail down the end points. 79 */ 80 if (o.commitspec != null) { 81 libgit2.example.common.check_lg2(libgit2.revparse.git_revparse(&revspec, repo, o.commitspec), "Couldn't parse commit spec", null); 82 83 if (revspec.flags & libgit2.revparse.git_revspec_t.GIT_REVSPEC_SINGLE) { 84 libgit2.oid.git_oid_cpy(&blameopts.newest_commit, libgit2.object.git_object_id(revspec.from)); 85 libgit2.object.git_object_free(revspec.from); 86 } else { 87 libgit2.oid.git_oid_cpy(&blameopts.oldest_commit, libgit2.object.git_object_id(revspec.from)); 88 libgit2.oid.git_oid_cpy(&blameopts.newest_commit, libgit2.object.git_object_id(revspec.to)); 89 libgit2.object.git_object_free(revspec.from); 90 libgit2.object.git_object_free(revspec.to); 91 } 92 } 93 94 /** Run the blame. */ 95 libgit2.blame.git_blame* blame = null; 96 libgit2.example.common.check_lg2(libgit2.blame.git_blame_file(&blame, repo, o.path, &blameopts), "Blame error", null); 97 98 char[1024] spec = '\0'; 99 100 /** 101 * Get the raw data inside the blob for output. We use the 102 * `committish:path/to/file.txt` format to find it. 103 */ 104 if (libgit2.oid.git_oid_is_zero(&blameopts.newest_commit)) { 105 core.stdc..string.strcpy(&(spec[0]), "HEAD"); 106 } else { 107 libgit2.oid.git_oid_tostr(&(spec[0]), spec.length, &blameopts.newest_commit); 108 } 109 110 core.stdc..string.strcat(&(spec[0]), ":"); 111 core.stdc..string.strcat(&(spec[0]), o.path); 112 113 libgit2.types.git_object* obj; 114 libgit2.example.common.check_lg2(libgit2.revparse.git_revparse_single(&obj, repo, &(spec[0])), "Object lookup error", null); 115 libgit2.types.git_blob* blob; 116 libgit2.example.common.check_lg2(libgit2.blob.git_blob_lookup(&blob, repo, libgit2.object.git_object_id(obj)), "Blob lookup error", null); 117 libgit2.object.git_object_free(obj); 118 119 const char* rawdata = cast(const char*)(libgit2.blob.git_blob_rawcontent(blob)); 120 libgit2.types.git_object_size_t rawsize = libgit2.blob.git_blob_rawsize(blob); 121 122 /** Produce the output. */ 123 int line = 1; 124 libgit2.types.git_object_size_t i = 0; 125 int break_on_null_hunk = 0; 126 127 while (i < rawsize) { 128 const char* eol = cast(const char*)(core.stdc..string.memchr(rawdata + i, '\n', cast(size_t)(rawsize - i))); 129 char[10] oid = '\0'; 130 const (libgit2.blame.git_blame_hunk)* hunk = libgit2.blame.git_blame_get_hunk_byline(blame, line); 131 132 if ((break_on_null_hunk) && (!hunk)) { 133 break; 134 } 135 136 if (hunk != null) { 137 char[128] sig = '\0'; 138 break_on_null_hunk = 1; 139 140 libgit2.oid.git_oid_tostr(&(oid[0]), 10, &hunk.final_commit_id); 141 libgit2.example.common.snprintf(&(sig[0]), 30, "%s <%s>", hunk.final_signature.name, hunk.final_signature.email); 142 143 core.stdc.stdio.printf("%s ( %-30s %3d) %.*s\n", &(oid[0]), &(sig[0]), line, cast(int)(eol - rawdata - i), rawdata + i); 144 } 145 146 i = cast(int)(eol - rawdata + 1); 147 line++; 148 } 149 150 /** Cleanup. */ 151 libgit2.blob.git_blob_free(blob); 152 libgit2.blame.git_blame_free(blame); 153 154 return 0; 155 } 156 157 /** 158 * Tell the user how to make this thing work. 159 */ 160 nothrow @nogc 161 private void usage(const (char)* msg, const (char)* arg) 162 163 in 164 { 165 } 166 167 do 168 { 169 if ((msg != null) && (arg != null)) { 170 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s: %s\n", msg, arg); 171 } else if (msg != null) { 172 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s\n", msg); 173 } 174 175 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "usage: blame [options] [<commit range>] <path>\n"); 176 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "\n"); 177 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, " <commit range> example: `HEAD~10..HEAD`, or `1234abcd`\n"); 178 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, " -L <n,m> process only line range n-m, counting from 1\n"); 179 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, " -M find line moves within and across files\n"); 180 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, " -C find line copies within and across files\n"); 181 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, " -F follow only the first parent commits\n"); 182 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "\n"); 183 core.stdc.stdlib.exit(1); 184 } 185 186 pragma(inline, true) 187 pure nothrow @trusted @nogc 188 private bool is_option(const char* input, char c1, char c2) 189 190 in 191 { 192 assert(input != null); 193 assert(c1 != c2); 194 assert(std.ascii.isUpper(c1)); 195 assert(std.ascii.isLower(c2)); 196 assert(c1 == std.ascii.toUpper(c2)); 197 } 198 199 do 200 { 201 return (*input == '-') && ((*(input + 1) == c1) || (*(input + 1) == c2)); 202 } 203 204 /** 205 * Parse the arguments. 206 */ 207 nothrow @nogc 208 private void parse_opts(.blame_opts* o, int argc, char** argv) 209 210 in 211 { 212 } 213 214 do 215 { 216 char*[3] bare_args = null; 217 218 if (argc < 2) { 219 .usage(null, null); 220 } 221 222 for (int i = 1; i < argc; i++) { 223 char* a = argv[i]; 224 225 if (a[0] != '-') { 226 i = 0; 227 228 while ((bare_args[i]) && (i < 3)) { 229 ++i; 230 } 231 232 if (i >= 3) { 233 .usage("Invalid argument set", null); 234 } 235 236 bare_args[i] = a; 237 } else if (!core.stdc..string.strcmp(a, "--")) { 238 continue; 239 } else if (.is_option(a, 'M', 'm')) { 240 o.M = 1; 241 } else if (.is_option(a, 'C', 'c')) { 242 o.C = 1; 243 } else if (.is_option(a, 'F', 'f')) { 244 o.F = 1; 245 } else if (.is_option(a, 'L', 'l')) { 246 i++; 247 a = argv[i]; 248 249 if (i >= argc) { 250 libgit2.example.common.fatal("Not enough arguments to -L", null); 251 } 252 253 libgit2.example.common.check_lg2(core.stdc.stdio.sscanf(a, "%d,%d", &o.start_line, &o.end_line) - 2, "-L format error", null); 254 } else { 255 /* commit range */ 256 if (o.commitspec) { 257 libgit2.example.common.fatal("Only one commit spec allowed", null); 258 } 259 260 o.commitspec = a; 261 } 262 } 263 264 /* Handle the bare arguments */ 265 if (!bare_args[0]) { 266 .usage("Please specify a path", null); 267 } 268 269 o.path = bare_args[0]; 270 271 if (bare_args[1]) { 272 /* <commitspec> <path> */ 273 o.path = bare_args[1]; 274 o.commitspec = bare_args[0]; 275 } 276 277 if (bare_args[2]) { 278 /* <oldcommit> <newcommit> <path> */ 279 char[128] spec = '\0'; 280 o.path = bare_args[2]; 281 core.stdc.stdio.sprintf(&(spec[0]), "%s..%s", bare_args[0], bare_args[1]); 282 o.commitspec = &(spec[0]); 283 } 284 }