1 /* 2 * libgit2 "diff" example - shows how to use the diff 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 the use of the libgit2 diff APIs to 16 * create `libgit2.diff.git_diff` objects and display them, emulating a number of 17 * core Git `diff` command line options. 18 * 19 * This covers on a portion of the core Git diff options and doesn't 20 * have particularly good error handling, but it should show most of 21 * the core libgit2 diff APIs, including various types of diffs and 22 * how to do renaming detection and patch formatting. 23 * 24 * License: $(LINK2 https://creativecommons.org/publicdomain/zero/1.0/, CC0 1.0 Universal) 25 */ 26 module libgit2.example.diff; 27 28 29 private static import core.stdc.stdio; 30 private static import core.stdc.stdlib; 31 private static import core.stdc.string; 32 private static import libgit2.buffer; 33 private static import libgit2.diff; 34 private static import libgit2.example.args; 35 private static import libgit2.example.common; 36 private static import libgit2.patch; 37 private static import libgit2.tree; 38 private static import libgit2.types; 39 40 private const (char)*[] colors = 41 [ 42 "\033[m", /* reset */ 43 "\033[1m", /* bold */ 44 "\033[31m", /* red */ 45 "\033[32m", /* green */ 46 "\033[36m", /* cyan */ 47 ]; 48 49 public enum 50 { 51 OUTPUT_DIFF = 1 << 0, 52 OUTPUT_STAT = 1 << 1, 53 OUTPUT_SHORTSTAT = 1 << 2, 54 OUTPUT_NUMSTAT = 1 << 3, 55 OUTPUT_SUMMARY = 1 << 4, 56 } 57 58 public enum 59 { 60 CACHE_NORMAL = 0, 61 CACHE_ONLY = 1, 62 CACHE_NONE = 2, 63 } 64 65 /** 66 * The 'diff_options' struct captures all the various parsed command line options. 67 */ 68 extern (C) 69 public struct diff_options 70 { 71 libgit2.diff.git_diff_options diffopts; 72 libgit2.diff.git_diff_find_options findopts; 73 int color; 74 int no_index; 75 int cache; 76 int output; 77 libgit2.diff.git_diff_format_t format = cast(libgit2.diff.git_diff_format_t)(0); 78 const (char)* treeish1; 79 const (char)* treeish2; 80 const (char)* dir; 81 } 82 83 /* 84 * These functions are implemented at the end 85 * 86 * - usage() 87 * - parse_opts() 88 * - color_printer() 89 * - diff_print_stats() 90 * - compute_diff_no_index() 91 */ 92 93 extern (C) 94 nothrow @nogc 95 public int lg2_diff(libgit2.types.git_repository* repo, int argc, char** argv) 96 97 in 98 { 99 } 100 101 do 102 { 103 libgit2.types.git_tree* t1 = null; 104 libgit2.types.git_tree* t2 = null; 105 libgit2.diff.git_diff* diff; 106 107 .diff_options o = {libgit2.diff.GIT_DIFF_OPTIONS_INIT(), libgit2.diff.GIT_DIFF_FIND_OPTIONS_INIT(), -1, -1, 0, 0, libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_PATCH, null, null, "."}; 108 109 .parse_opts(&o, argc, argv); 110 111 /** 112 * Possible argument patterns: 113 * 114 * * <sha1> <sha2> 115 * * <sha1> --cached 116 * * <sha1> 117 * * --cached 118 * * --nocache (don't use index data in diff at all) 119 * * --no-index <file1> <file2> 120 * * nothing 121 * 122 * Currently ranged arguments like <sha1>..<sha2> and <sha1>...<sha2> 123 * are not supported in this example 124 */ 125 126 if (o.no_index >= 0) { 127 .compute_diff_no_index(&diff, &o); 128 } else { 129 if (o.treeish1 != null) { 130 libgit2.example.common.treeish_to_tree(&t1, repo, o.treeish1); 131 } 132 133 if (o.treeish2 != null) { 134 libgit2.example.common.treeish_to_tree(&t2, repo, o.treeish2); 135 } 136 137 if ((t1 != null) && (t2 != null)) { 138 libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_tree(&diff, repo, t1, t2, &o.diffopts), "diff trees", null); 139 } else if (o.cache != .CACHE_NORMAL) { 140 if (t1 == null) { 141 libgit2.example.common.treeish_to_tree(&t1, repo, "HEAD"); 142 } 143 144 if (o.cache == .CACHE_NONE) { 145 libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_workdir(&diff, repo, t1, &o.diffopts), "diff tree to working directory", null); 146 } else { 147 libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_index(&diff, repo, t1, null, &o.diffopts), "diff tree to index", null); 148 } 149 } else if (t1 != null) { 150 libgit2.example.common.check_lg2(libgit2.diff.git_diff_tree_to_workdir_with_index(&diff, repo, t1, &o.diffopts), "diff tree to working directory", null); 151 } else { 152 libgit2.example.common.check_lg2(libgit2.diff.git_diff_index_to_workdir(&diff, repo, null, &o.diffopts), "diff index to working directory", null); 153 } 154 155 /** Apply rename and copy detection if requested. */ 156 157 if ((o.findopts.flags & libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_ALL) != 0) { 158 libgit2.example.common.check_lg2(libgit2.diff.git_diff_find_similar(diff, &o.findopts), "finding renames and copies", null); 159 } 160 } 161 162 /** Generate simple output using libgit2 display helper. */ 163 164 if (!o.output) { 165 o.output = .OUTPUT_DIFF; 166 } 167 168 if (o.output != .OUTPUT_DIFF) { 169 .diff_print_stats(diff, &o); 170 } 171 172 if ((o.output & .OUTPUT_DIFF) != 0) { 173 if (o.color >= 0) { 174 core.stdc.stdio.fputs(.colors[0], core.stdc.stdio.stdout); 175 } 176 177 libgit2.example.common.check_lg2(libgit2.diff.git_diff_print(diff, o.format, &.color_printer, &o.color), "displaying diff", null); 178 179 if (o.color >= 0) { 180 core.stdc.stdio.fputs(.colors[0], core.stdc.stdio.stdout); 181 } 182 } 183 184 /** Cleanup before exiting. */ 185 libgit2.diff.git_diff_free(diff); 186 libgit2.tree.git_tree_free(t1); 187 libgit2.tree.git_tree_free(t2); 188 189 return 0; 190 } 191 192 nothrow @nogc 193 private void compute_diff_no_index(libgit2.diff.git_diff** diff, .diff_options* o) 194 195 in 196 { 197 } 198 199 do 200 { 201 if ((!o.treeish1) || (!o.treeish2)) { 202 .usage("two files should be provided as arguments", null); 203 } 204 205 char* file1_str = libgit2.example.common.read_file(o.treeish1); 206 207 if (file1_str == null) { 208 .usage("file cannot be read", o.treeish1); 209 } 210 211 char* file2_str = libgit2.example.common.read_file(o.treeish2); 212 213 if (file2_str == null) { 214 .usage("file cannot be read", o.treeish2); 215 } 216 217 libgit2.patch.git_patch* patch = null; 218 libgit2.example.common.check_lg2(libgit2.patch.git_patch_from_buffers(&patch, file1_str, core.stdc..string.strlen(file1_str), o.treeish1, file2_str, core.stdc..string.strlen(file2_str), o.treeish2, &o.diffopts), "patch buffers", null); 219 libgit2.buffer.git_buf buf = libgit2.buffer.git_buf.init; 220 libgit2.example.common.check_lg2(libgit2.patch.git_patch_to_buf(&buf, patch), "patch to buf", null); 221 libgit2.example.common.check_lg2(libgit2.diff.git_diff_from_buffer(diff, buf.ptr_, buf.size), "diff from patch", null); 222 libgit2.patch.git_patch_free(patch); 223 libgit2.buffer.git_buf_dispose(&buf); 224 core.stdc.stdlib.free(file1_str); 225 core.stdc.stdlib.free(file2_str); 226 } 227 228 nothrow @nogc 229 private void usage(const (char)* message, const (char)* arg) 230 231 in 232 { 233 } 234 235 do 236 { 237 if ((message != null) && (arg != null)) { 238 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s: %s\n", message, arg); 239 } else if (message != null) { 240 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "%s\n", message); 241 } 242 243 core.stdc.stdio.fprintf(core.stdc.stdio.stderr, "usage: diff [<tree-oid> [<tree-oid>]]\n"); 244 core.stdc.stdlib.exit(1); 245 } 246 247 /** 248 * This implements very rudimentary colorized output. 249 */ 250 extern (C) 251 nothrow @nogc 252 private int color_printer(const (libgit2.diff.git_diff_delta)* delta, const (libgit2.diff.git_diff_hunk)* hunk, const (libgit2.diff.git_diff_line)* line, void* data) 253 254 in 255 { 256 } 257 258 do 259 { 260 int* last_color = cast(int*)(data); 261 int color = 0; 262 263 //cast(void)(delta); 264 //cast(void)(hunk); 265 266 if (*last_color >= 0) { 267 switch (line.origin) { 268 case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_ADDITION: 269 color = 3; 270 271 break; 272 273 case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_DELETION: 274 color = 2; 275 276 break; 277 278 case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_ADD_EOFNL: 279 color = 3; 280 281 break; 282 283 case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_DEL_EOFNL: 284 color = 2; 285 286 break; 287 288 case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_FILE_HDR: 289 color = 1; 290 291 break; 292 293 case libgit2.diff.git_diff_line_t.GIT_DIFF_LINE_HUNK_HDR: 294 color = 4; 295 296 break; 297 298 default: 299 break; 300 } 301 302 if (color != *last_color) { 303 if ((*last_color == 1) || (color == 1)) { 304 core.stdc.stdio.fputs(.colors[0], core.stdc.stdio.stdout); 305 } 306 307 core.stdc.stdio.fputs(.colors[color], core.stdc.stdio.stdout); 308 *last_color = color; 309 } 310 } 311 312 return libgit2.example.common.diff_output(delta, hunk, line, cast(void*)(core.stdc.stdio.stdout)); 313 } 314 315 /** 316 * Parse arguments as copied from git-diff. 317 */ 318 nothrow @nogc 319 private void parse_opts(.diff_options* o, int argc, char** argv) 320 321 in 322 { 323 } 324 325 do 326 { 327 libgit2.example.args.args_info args = libgit2.example.args.ARGS_INFO_INIT(argc, argv); 328 329 for (args.pos = 1; args.pos < argc; ++args.pos) { 330 const (char)* a = argv[args.pos]; 331 332 if (a[0] != '-') { 333 if (o.treeish1 == null) { 334 o.treeish1 = a; 335 } else if (o.treeish2 == null) { 336 o.treeish2 = a; 337 } else { 338 .usage("Only one or two tree identifiers can be provided", null); 339 } 340 } else if ((!core.stdc..string.strcmp(a, "-p")) || (!core.stdc..string.strcmp(a, "-u")) || (!core.stdc..string.strcmp(a, "--patch"))) { 341 o.output |= .OUTPUT_DIFF; 342 o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_PATCH; 343 } else if (!core.stdc..string.strcmp(a, "--cached")) { 344 o.cache = .CACHE_ONLY; 345 346 if (o.no_index >= 0) { 347 .usage("--cached and --no-index are incompatible", null); 348 } 349 } else if (!core.stdc..string.strcmp(a, "--nocache")) { 350 o.cache = .CACHE_NONE; 351 } else if ((!core.stdc..string.strcmp(a, "--name-only")) || (!core.stdc..string.strcmp(a, "--format=name"))) { 352 o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_NAME_ONLY; 353 } else if ((!core.stdc..string.strcmp(a, "--name-status")) || (!core.stdc..string.strcmp(a, "--format=name-status"))) { 354 o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_NAME_STATUS; 355 } else if ((!core.stdc..string.strcmp(a, "--raw")) || (!core.stdc..string.strcmp(a, "--format=raw"))) { 356 o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_RAW; 357 } else if (!core.stdc..string.strcmp(a, "--format=diff-index")) { 358 o.format = libgit2.diff.git_diff_format_t.GIT_DIFF_FORMAT_RAW; 359 o.diffopts.id_abbrev = 40; 360 } else if (!core.stdc..string.strcmp(a, "--no-index")) { 361 o.no_index = 0; 362 363 if (o.cache == .CACHE_ONLY) { 364 .usage("--cached and --no-index are incompatible", null); 365 } 366 } else if (!core.stdc..string.strcmp(a, "--color")) { 367 o.color = 0; 368 } else if (!core.stdc..string.strcmp(a, "--no-color")) { 369 o.color = -1; 370 } else if (!core.stdc..string.strcmp(a, "-R")) { 371 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_REVERSE; 372 } else if ((!core.stdc..string.strcmp(a, "-a")) || (!core.stdc..string.strcmp(a, "--text"))) { 373 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_FORCE_TEXT; 374 } else if (!core.stdc..string.strcmp(a, "--ignore-space-at-eol")) { 375 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_IGNORE_WHITESPACE_EOL; 376 } else if ((!core.stdc..string.strcmp(a, "-b")) || (!core.stdc..string.strcmp(a, "--ignore-space-change"))) { 377 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_IGNORE_WHITESPACE_CHANGE; 378 } else if ((!core.stdc..string.strcmp(a, "-w")) || (!core.stdc..string.strcmp(a, "--ignore-all-space"))) { 379 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_IGNORE_WHITESPACE; 380 } else if (!core.stdc..string.strcmp(a, "--ignored")) { 381 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_INCLUDE_IGNORED; 382 } else if (!core.stdc..string.strcmp(a, "--untracked")) { 383 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_INCLUDE_UNTRACKED; 384 } else if (!core.stdc..string.strcmp(a, "--patience")) { 385 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_PATIENCE; 386 } else if (!core.stdc..string.strcmp(a, "--minimal")) { 387 o.diffopts.flags |= libgit2.diff.git_diff_option_t.GIT_DIFF_MINIMAL; 388 } else if (!core.stdc..string.strcmp(a, "--stat")) { 389 o.output |= .OUTPUT_STAT; 390 } else if (!core.stdc..string.strcmp(a, "--numstat")) { 391 o.output |= .OUTPUT_NUMSTAT; 392 } else if (!core.stdc..string.strcmp(a, "--shortstat")) { 393 o.output |= .OUTPUT_SHORTSTAT; 394 } else if (!core.stdc..string.strcmp(a, "--summary")) { 395 o.output |= .OUTPUT_SUMMARY; 396 } else if (libgit2.example.args.match_uint16_arg(&o.findopts.rename_threshold, &args, "-M") || libgit2.example.args.match_uint16_arg(&o.findopts.rename_threshold, &args, "--find-renames")) { 397 o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_RENAMES; 398 } else if (libgit2.example.args.match_uint16_arg(&o.findopts.copy_threshold, &args, "-C") || libgit2.example.args.match_uint16_arg(&o.findopts.copy_threshold, &args, "--find-copies")) { 399 o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_COPIES; 400 } else if (!core.stdc..string.strcmp(a, "--find-copies-harder")) { 401 o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED; 402 } else if (libgit2.example.args.is_prefixed(a, "-B") || libgit2.example.args.is_prefixed(a, "--break-rewrites")) { 403 /* TODO: parse thresholds */ 404 o.findopts.flags |= libgit2.diff.git_diff_find_t.GIT_DIFF_FIND_REWRITES; 405 } else if ((!libgit2.example.args.match_uint32_arg(&o.diffopts.context_lines, &args, "-U")) && (!libgit2.example.args.match_uint32_arg(&o.diffopts.context_lines, &args, "--unified")) && (!libgit2.example.args.match_uint32_arg(&o.diffopts.interhunk_lines, &args, "--inter-hunk-context")) && !libgit2.example.args.match_uint16_arg(&o.diffopts.id_abbrev, &args, "--abbrev") && !libgit2.example.args.match_str_arg(&o.diffopts.old_prefix, &args, "--src-prefix") && (!libgit2.example.args.match_str_arg(&o.diffopts.new_prefix, &args, "--dst-prefix")) && (!libgit2.example.args.match_str_arg(&o.dir, &args, "--git-dir"))) { 406 .usage("Unknown command line argument", a); 407 } 408 } 409 } 410 411 /** 412 * Display diff output with "--stat", "--numstat", or "--shortstat" 413 */ 414 nothrow @nogc 415 private void diff_print_stats(libgit2.diff.git_diff* diff, .diff_options* o) 416 417 in 418 { 419 } 420 421 do 422 { 423 libgit2.diff.git_diff_stats* stats; 424 libgit2.example.common.check_lg2(libgit2.diff.git_diff_get_stats(&stats, diff), "generating stats for diff", null); 425 426 libgit2.diff.git_diff_stats_format_t format = libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_NONE; 427 428 if (o.output & .OUTPUT_STAT) { 429 format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_FULL; 430 } 431 432 if (o.output & .OUTPUT_SHORTSTAT) { 433 format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_SHORT; 434 } 435 436 if (o.output & .OUTPUT_NUMSTAT) { 437 format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_NUMBER; 438 } 439 440 if (o.output & .OUTPUT_SUMMARY) { 441 format |= libgit2.diff.git_diff_stats_format_t.GIT_DIFF_STATS_INCLUDE_SUMMARY; 442 } 443 444 libgit2.buffer.git_buf b; 445 libgit2.example.common.check_lg2(libgit2.diff.git_diff_stats_to_buf(&b, stats, format, 80), "formatting stats", null); 446 447 b = libgit2.buffer.GIT_BUF_INIT(); 448 core.stdc.stdio.fputs(b.ptr_, core.stdc.stdio.stdout); 449 450 libgit2.buffer.git_buf_dispose(&b); 451 libgit2.diff.git_diff_stats_free(stats); 452 }