/* * The MIT License * * Copyright (c) 2014 GitHub, Inc * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "rugged.h" extern VALUE rb_mRugged; VALUE rb_cRuggedDiff; VALUE rugged_diff_new(VALUE klass, VALUE owner, git_diff *diff) { VALUE rb_diff = Data_Wrap_Struct(klass, NULL, git_diff_free, diff); rugged_set_owner(rb_diff, owner); return rb_diff; } /** * The caller has to free the returned git_diff_options pathspec strings array. */ void rugged_parse_diff_options(git_diff_options *opts, VALUE rb_options) { if (!NIL_P(rb_options)) { VALUE rb_value; Check_Type(rb_options, T_HASH); if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("reverse")))) { opts->flags |= GIT_DIFF_REVERSE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("include_ignored")))) { opts->flags |= GIT_DIFF_INCLUDE_IGNORED; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("recurse_ignored_dirs")))) { opts->flags |= GIT_DIFF_RECURSE_IGNORED_DIRS; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("include_untracked")))) { opts->flags |= GIT_DIFF_INCLUDE_UNTRACKED; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("recurse_untracked_dirs")))) { opts->flags |= GIT_DIFF_RECURSE_UNTRACKED_DIRS; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("include_unmodified")))) { opts->flags |= GIT_DIFF_INCLUDE_UNMODIFIED; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("include_typechange")))) { opts->flags |= GIT_DIFF_INCLUDE_TYPECHANGE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("include_typechange_trees")))) { opts->flags |= GIT_DIFF_INCLUDE_TYPECHANGE_TREES; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("ignore_filemode")))) { opts->flags |= GIT_DIFF_IGNORE_FILEMODE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("ignore_submodules")))) { opts->flags |= GIT_DIFF_IGNORE_SUBMODULES; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("ignore_case")))) { opts->flags |= GIT_DIFF_IGNORE_CASE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("disable_pathspec_match")))) { opts->flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("skip_binary_check")))) { opts->flags |= GIT_DIFF_SKIP_BINARY_CHECK; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("fast_untracked_dirs")))) { opts->flags |= GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("force_text")))) { opts->flags |= GIT_DIFF_FORCE_TEXT; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("force_binary")))) { opts->flags |= GIT_DIFF_FORCE_BINARY; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("ignore_whitespace")))) { opts->flags |= GIT_DIFF_IGNORE_WHITESPACE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("ignore_whitespace_change")))) { opts->flags |= GIT_DIFF_IGNORE_WHITESPACE_CHANGE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("ignore_whitespace_eol")))) { opts->flags |= GIT_DIFF_IGNORE_WHITESPACE_EOL; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("show_untracked_content")))) { opts->flags |= GIT_DIFF_SHOW_UNTRACKED_CONTENT; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("show_unmodified")))) { opts->flags |= GIT_DIFF_SHOW_UNMODIFIED; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("show_binary")))) { opts->flags |= GIT_DIFF_SHOW_BINARY; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("patience")))) { opts->flags |= GIT_DIFF_PATIENCE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("minimal")))) { opts->flags |= GIT_DIFF_MINIMAL; } rb_value = rb_hash_aref(rb_options, CSTR2SYM("paths")); if (!NIL_P(rb_value)) { int i; Check_Type(rb_value, T_ARRAY); for (i = 0; i < RARRAY_LEN(rb_value); ++i) Check_Type(rb_ary_entry(rb_value, i), T_STRING); opts->pathspec.count = RARRAY_LEN(rb_value); opts->pathspec.strings = xmalloc(opts->pathspec.count * sizeof(char *)); for (i = 0; i < RARRAY_LEN(rb_value); ++i) { VALUE rb_path = rb_ary_entry(rb_value, i); opts->pathspec.strings[i] = StringValueCStr(rb_path); } } rb_value = rb_hash_aref(rb_options, CSTR2SYM("context_lines")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts->context_lines = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("interhunk_lines")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts->interhunk_lines = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("id_abbrev")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts->id_abbrev = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("max_size")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts->max_size = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("old_prefix")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_STRING); opts->old_prefix = StringValueCStr(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("new_prefix")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_STRING); opts->new_prefix = StringValueCStr(rb_value); } } } static int diff_print_cb( const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload) { VALUE rb_str = (VALUE)payload; switch (line->origin) { case GIT_DIFF_LINE_CONTEXT: case GIT_DIFF_LINE_ADDITION: case GIT_DIFF_LINE_DELETION: rb_str_cat(rb_str, &line->origin, 1); } rb_str_cat(rb_str, line->content, line->content_len); return GIT_OK; } /* * call-seq: * diff.patch -> patch * diff.patch(:compact => true) -> compact_patch * * Return a string containing the diff in patch form. */ static VALUE rb_git_diff_patch(int argc, VALUE *argv, VALUE self) { git_diff *diff; VALUE rb_str = rb_str_new(NULL, 0); VALUE rb_opts; rb_scan_args(argc, argv, "00:", &rb_opts); Data_Get_Struct(self, git_diff, diff); if (!NIL_P(rb_opts)) { if (rb_hash_aref(rb_opts, CSTR2SYM("compact")) == Qtrue) git_diff_print(diff, GIT_DIFF_FORMAT_NAME_STATUS, diff_print_cb, (void*)rb_str); else git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, diff_print_cb, (void*)rb_str); } else { git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, diff_print_cb, (void*)rb_str); } return rb_str; } static int diff_write_cb( const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload) { VALUE rb_io = (VALUE)payload, str = rb_str_new(line->content, line->content_len); rb_io_write(rb_io, str); return GIT_OK; } /* * call-seq: * diff.write_patch(io) -> nil * diff.write_patch(io, :compact => true) -> nil * * Write a patch directly to an object which responds to "write". */ static VALUE rb_git_diff_write_patch(int argc, VALUE *argv, VALUE self) { git_diff *diff; VALUE rb_io, rb_opts; rb_scan_args(argc, argv, "10:", &rb_io, &rb_opts); if (!rb_respond_to(rb_io, rb_intern("write"))) rb_raise(rb_eArgError, "Expected io to respond to \"write\""); Data_Get_Struct(self, git_diff, diff); if (!NIL_P(rb_opts)) { if (rb_hash_aref(rb_opts, CSTR2SYM("compact")) == Qtrue) git_diff_print(diff, GIT_DIFF_FORMAT_NAME_STATUS, diff_write_cb, (void*)rb_io); else git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, diff_write_cb, (void*)rb_io); } else { git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, diff_write_cb, (void*)rb_io); } return Qnil; } /* * call-seq: * diff.merge!(other_diff) -> self * * Merges all diff information from +other_diff+. */ static VALUE rb_git_diff_merge(VALUE self, VALUE rb_other) { git_diff *diff; git_diff *other; int error; if (!rb_obj_is_kind_of(rb_other, rb_cRuggedDiff)) rb_raise(rb_eTypeError, "A Rugged::Diff instance is required"); Data_Get_Struct(self, git_diff, diff); Data_Get_Struct(rb_other, git_diff, other); error = git_diff_merge(diff, other); rugged_exception_check(error); return self; } /* * call-seq: * diff.find_similar!([options]) -> self * * Detects entries in the diff that look like renames or copies (based on the * given options) and replaces them with actual rename or copy entries. * * Additionally, modified files can be broken into add/delete pairs if the * amount of changes are above a specific threshold (see +:break_rewrite_threshold+). * * By default, similarity will be measured without leading whitespace. You * you can use the +:dont_ignore_whitespace+ to disable this. * * The following options can be passed in the +options+ Hash: * * :rename_threshold :: * An integer specifying the similarity to consider a file renamed (default 50). * * :rename_from_rewrite_threshold :: * An integer specifying the similarity of modified to be eligible * rename source (default 50). * * :copy_threshold :: * An integer specifying the similarity to consider a file a copy (default 50). * * :break_rewrite_threshold :: * An integer specifying the similarity to split modify into delete/add pair (default 60). * * :rename_limit :: * An integer specifying the maximum amount of similarity sources to examine * (a la diff's +-l+ option or the +diff.renameLimit+ config) (default 200). * * :renames :: * If true, looking for renames will be enabled (+--find-renames+). * * :renames_from_rewrites :: * If true, the "old side" of modified files will be considered for renames (+--break-rewrites=N+). * * :copies :: * If true, looking for copies will be enabled (+--find-copies+). * * :copies_from_unmodified :: * If true, unmodified files will be considered as copy sources (+--find-copies-harder+). * * :break_rewrites :: * If true, larger rewrites will be split into delete/add pairs (+--break-rewrites=/M+). * * :all :: * If true, enables all finding features. * * :ignore_whitespace :: * If true, similarity will be measured with all whitespace ignored. * * :dont_ignore_whitespace :: * If true, similarity will be measured without ignoring any whitespace. * */ static VALUE rb_git_diff_find_similar(int argc, VALUE *argv, VALUE self) { git_diff *diff; git_diff_find_options opts = GIT_DIFF_FIND_OPTIONS_INIT; VALUE rb_options; int error; Data_Get_Struct(self, git_diff, diff); rb_scan_args(argc, argv, "00:", &rb_options); if (!NIL_P(rb_options)) { VALUE rb_value = rb_hash_aref(rb_options, CSTR2SYM("rename_threshold")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts.rename_threshold = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("rename_from_rewrite_threshold")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts.rename_from_rewrite_threshold = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("copy_threshold")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts.copy_threshold = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("break_rewrite_threshold")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts.break_rewrite_threshold = FIX2INT(rb_value); } rb_value = rb_hash_aref(rb_options, CSTR2SYM("rename_limit")); if (!NIL_P(rb_value)) { Check_Type(rb_value, T_FIXNUM); opts.rename_limit = FIX2INT(rb_value); } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("renames")))) { opts.flags |= GIT_DIFF_FIND_RENAMES; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("renames_from_rewrites")))) { opts.flags |= GIT_DIFF_FIND_RENAMES_FROM_REWRITES; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("copies")))) { opts.flags |= GIT_DIFF_FIND_COPIES; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("copies_from_unmodified")))) { opts.flags |= GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("break_rewrites")))) { opts.flags |= GIT_DIFF_FIND_AND_BREAK_REWRITES; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("all")))) { opts.flags |= GIT_DIFF_FIND_ALL; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("ignore_whitespace")))) { opts.flags |= GIT_DIFF_FIND_IGNORE_WHITESPACE; } if (RTEST(rb_hash_aref(rb_options, CSTR2SYM("dont_ignore_whitespace")))) { opts.flags |= GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE; } } error = git_diff_find_similar(diff, &opts); rugged_exception_check(error); return self; } /* * call-seq: * diff.each_patch { |patch| } -> self * diff.each_patch -> enumerator * * If given a block, yields each patch that is part of the diff. * If no block is given, an enumerator will be returned. */ static VALUE rb_git_diff_each_patch(VALUE self) { git_diff *diff; git_patch *patch; int error = 0; size_t d, delta_count; if (!rb_block_given_p()) { return rb_funcall(self, rb_intern("to_enum"), 1, CSTR2SYM("each_patch"), self); } Data_Get_Struct(self, git_diff, diff); delta_count = git_diff_num_deltas(diff); for (d = 0; d < delta_count; ++d) { error = git_patch_from_diff(&patch, diff, d); if (error) break; rb_yield(rugged_patch_new(self, patch)); } rugged_exception_check(error); return self; } /* * call-seq: * diff.each_delta { |delta| } -> self * diff.each_delta -> enumerator * * If given a block, yields each delta that is part of the diff. * If no block is given, an enumerator will be returned. * * This method should be preferred over #each_patch if you're not interested * in the actual line-by-line changes of the diff. */ static VALUE rb_git_diff_each_delta(VALUE self) { git_diff *diff; const git_diff_delta *delta; int error = 0; size_t d, delta_count; if (!rb_block_given_p()) { return rb_funcall(self, rb_intern("to_enum"), 1, CSTR2SYM("each_delta"), self); } Data_Get_Struct(self, git_diff, diff); delta_count = git_diff_num_deltas(diff); for (d = 0; d < delta_count; ++d) { delta = git_diff_get_delta(diff, d); rb_yield(rugged_diff_delta_new(self, delta)); } rugged_exception_check(error); return self; } static int each_line_cb( const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload) { int *exception = (int *)payload; rb_protect(rb_yield, rugged_diff_line_new(line), exception); return *exception ? GIT_ERROR : GIT_OK; } /* * call-seq: * diff.each_line([format = :patch]) { |line| } -> self * diff.each_line([format = :patch]) -> enumerator */ static VALUE rb_git_diff_each_line(int argc, VALUE *argv, VALUE self) { VALUE rb_format; git_diff *diff; git_diff_format_t format; int exception = 0, error; Data_Get_Struct(self, git_diff, diff); if (rb_scan_args(argc, argv, "01", &rb_format) == 1) { Check_Type(rb_format, T_SYMBOL); } else { rb_format = CSTR2SYM("patch"); } if (!rb_block_given_p()) return rb_funcall(self, rb_intern("to_enum"), 2, CSTR2SYM("each_line"), rb_format); if (SYM2ID(rb_format) == rb_intern("patch")) { format = GIT_DIFF_FORMAT_PATCH; } else if (SYM2ID(rb_format) == rb_intern("patch_header")) { format = GIT_DIFF_FORMAT_PATCH_HEADER; } else if (SYM2ID(rb_format) == rb_intern("raw")) { format = GIT_DIFF_FORMAT_RAW; } else if (SYM2ID(rb_format) == rb_intern("name_only")) { format = GIT_DIFF_FORMAT_NAME_ONLY; } else if (SYM2ID(rb_format) == rb_intern("name_status")) { format = GIT_DIFF_FORMAT_NAME_STATUS; } else { rb_raise(rb_eArgError, "unknown :format"); } error = git_diff_print(diff, format, each_line_cb, &exception); if (exception) rb_jump_tag(exception); rugged_exception_check(error); return self; } /* * call-seq: diff.size -> int * * Returns the number of deltas/patches in this diff. */ static VALUE rb_git_diff_size(VALUE self) { git_diff *diff; Data_Get_Struct(self, git_diff, diff); return INT2FIX(git_diff_num_deltas(diff)); } struct diff_stats { size_t files, adds, dels; }; static int diff_file_stats_cb( const git_diff_delta *delta, float progress, void *payload) { struct diff_stats *stats = payload; switch (delta->status) { case GIT_DELTA_ADDED: case GIT_DELTA_DELETED: case GIT_DELTA_MODIFIED: case GIT_DELTA_RENAMED: case GIT_DELTA_COPIED: case GIT_DELTA_TYPECHANGE: stats->files++; break; default: /* unmodified, ignored, and untracked files don't count */ break; } return GIT_OK; } static int diff_line_stats_cb( const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload) { struct diff_stats *stats = payload; switch (line->origin) { case GIT_DIFF_LINE_ADDITION: stats->adds++; break; case GIT_DIFF_LINE_DELETION: stats->dels++; break; default: break; } return GIT_OK; } /* * call-seq: diff.stat -> int, int, int * * Returns the number of files/additions/deletions in this diff. */ static VALUE rb_git_diff_stat(VALUE self) { git_diff *diff; struct diff_stats stats = { 0, 0, 0 }; Data_Get_Struct(self, git_diff, diff); git_diff_foreach( diff, diff_file_stats_cb, NULL, NULL, diff_line_stats_cb, &stats); return rb_ary_new3( 3, INT2FIX(stats.files), INT2FIX(stats.adds), INT2FIX(stats.dels)); } /* * call-seq: diff.sorted_icase? * * Returns true when deltas are sorted case insensitively. */ static VALUE rb_git_diff_sorted_icase_p(VALUE self) { git_diff *diff; Data_Get_Struct(self, git_diff, diff); return git_diff_is_sorted_icase(diff) ? Qtrue : Qfalse; } void Init_rugged_diff(void) { rb_cRuggedDiff = rb_define_class_under(rb_mRugged, "Diff", rb_cObject); rb_define_method(rb_cRuggedDiff, "patch", rb_git_diff_patch, -1); rb_define_method(rb_cRuggedDiff, "write_patch", rb_git_diff_write_patch, -1); rb_define_method(rb_cRuggedDiff, "find_similar!", rb_git_diff_find_similar, -1); rb_define_method(rb_cRuggedDiff, "merge!", rb_git_diff_merge, 1); rb_define_method(rb_cRuggedDiff, "size", rb_git_diff_size, 0); rb_define_method(rb_cRuggedDiff, "stat", rb_git_diff_stat, 0); rb_define_method(rb_cRuggedDiff, "sorted_icase?", rb_git_diff_sorted_icase_p, 0); rb_define_method(rb_cRuggedDiff, "each_patch", rb_git_diff_each_patch, 0); rb_define_method(rb_cRuggedDiff, "each_delta", rb_git_diff_each_delta, 0); rb_define_method(rb_cRuggedDiff, "each_line", rb_git_diff_each_line, -1); }