summaryrefslogtreecommitdiff
path: root/src/libgit2/diff_driver.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libgit2/diff_driver.c')
-rw-r--r--src/libgit2/diff_driver.c522
1 files changed, 522 insertions, 0 deletions
diff --git a/src/libgit2/diff_driver.c b/src/libgit2/diff_driver.c
new file mode 100644
index 000000000..5f25fdb44
--- /dev/null
+++ b/src/libgit2/diff_driver.c
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "diff_driver.h"
+
+#include "git2/attr.h"
+
+#include "common.h"
+#include "diff.h"
+#include "strmap.h"
+#include "map.h"
+#include "config.h"
+#include "regexp.h"
+#include "repository.h"
+
+typedef enum {
+ DIFF_DRIVER_AUTO = 0,
+ DIFF_DRIVER_BINARY = 1,
+ DIFF_DRIVER_TEXT = 2,
+ DIFF_DRIVER_PATTERNLIST = 3
+} git_diff_driver_t;
+
+typedef struct {
+ git_regexp re;
+ int flags;
+} git_diff_driver_pattern;
+
+enum {
+ REG_NEGATE = (1 << 15) /* get out of the way of existing flags */
+};
+
+/* data for finding function context for a given file type */
+struct git_diff_driver {
+ git_diff_driver_t type;
+ uint32_t binary_flags;
+ uint32_t other_flags;
+ git_array_t(git_diff_driver_pattern) fn_patterns;
+ git_regexp word_pattern;
+ char name[GIT_FLEX_ARRAY];
+};
+
+#include "userdiff.h"
+
+struct git_diff_driver_registry {
+ git_strmap *drivers;
+};
+
+#define FORCE_DIFFABLE (GIT_DIFF_FORCE_TEXT | GIT_DIFF_FORCE_BINARY)
+
+static git_diff_driver diff_driver_auto = { DIFF_DRIVER_AUTO, 0, 0 };
+static git_diff_driver diff_driver_binary = { DIFF_DRIVER_BINARY, GIT_DIFF_FORCE_BINARY, 0 };
+static git_diff_driver diff_driver_text = { DIFF_DRIVER_TEXT, GIT_DIFF_FORCE_TEXT, 0 };
+
+git_diff_driver_registry *git_diff_driver_registry_new(void)
+{
+ git_diff_driver_registry *reg =
+ git__calloc(1, sizeof(git_diff_driver_registry));
+ if (!reg)
+ return NULL;
+
+ if (git_strmap_new(&reg->drivers) < 0) {
+ git_diff_driver_registry_free(reg);
+ return NULL;
+ }
+
+ return reg;
+}
+
+void git_diff_driver_registry_free(git_diff_driver_registry *reg)
+{
+ git_diff_driver *drv;
+
+ if (!reg)
+ return;
+
+ git_strmap_foreach_value(reg->drivers, drv, git_diff_driver_free(drv));
+ git_strmap_free(reg->drivers);
+ git__free(reg);
+}
+
+static int diff_driver_add_patterns(
+ git_diff_driver *drv, const char *regex_str, int regex_flags)
+{
+ int error = 0;
+ const char *scan, *end;
+ git_diff_driver_pattern *pat = NULL;
+ git_str buf = GIT_STR_INIT;
+
+ for (scan = regex_str; scan; scan = end) {
+ /* get pattern to fill in */
+ if ((pat = git_array_alloc(drv->fn_patterns)) == NULL) {
+ return -1;
+ }
+
+ pat->flags = regex_flags;
+ if (*scan == '!') {
+ pat->flags |= REG_NEGATE;
+ ++scan;
+ }
+
+ if ((end = strchr(scan, '\n')) != NULL) {
+ error = git_str_set(&buf, scan, end - scan);
+ end++;
+ } else {
+ error = git_str_sets(&buf, scan);
+ }
+ if (error < 0)
+ break;
+
+ if ((error = git_regexp_compile(&pat->re, buf.ptr, regex_flags)) != 0) {
+ /*
+ * TODO: issue a warning
+ */
+ }
+ }
+
+ if (error && pat != NULL)
+ (void)git_array_pop(drv->fn_patterns); /* release last item */
+ git_str_dispose(&buf);
+
+ /* We want to ignore bad patterns, so return success regardless */
+ return 0;
+}
+
+static int diff_driver_xfuncname(const git_config_entry *entry, void *payload)
+{
+ return diff_driver_add_patterns(payload, entry->value, 0);
+}
+
+static int diff_driver_funcname(const git_config_entry *entry, void *payload)
+{
+ return diff_driver_add_patterns(payload, entry->value, 0);
+}
+
+static git_diff_driver_registry *git_repository_driver_registry(
+ git_repository *repo)
+{
+ git_diff_driver_registry *reg = git_atomic_load(repo->diff_drivers), *newreg;
+ if (reg)
+ return reg;
+
+ newreg = git_diff_driver_registry_new();
+ if (!newreg) {
+ git_error_set(GIT_ERROR_REPOSITORY, "unable to create diff driver registry");
+ return newreg;
+ }
+ reg = git_atomic_compare_and_swap(&repo->diff_drivers, NULL, newreg);
+ if (!reg) {
+ reg = newreg;
+ } else {
+ /* if we race, free losing allocation */
+ git_diff_driver_registry_free(newreg);
+ }
+ return reg;
+}
+
+static int diff_driver_alloc(
+ git_diff_driver **out, size_t *namelen_out, const char *name)
+{
+ git_diff_driver *driver;
+ size_t driverlen = sizeof(git_diff_driver),
+ namelen = strlen(name),
+ alloclen;
+
+ GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, driverlen, namelen);
+ GIT_ERROR_CHECK_ALLOC_ADD(&alloclen, alloclen, 1);
+
+ driver = git__calloc(1, alloclen);
+ GIT_ERROR_CHECK_ALLOC(driver);
+
+ memcpy(driver->name, name, namelen);
+
+ *out = driver;
+
+ if (namelen_out)
+ *namelen_out = namelen;
+
+ return 0;
+}
+
+static int git_diff_driver_builtin(
+ git_diff_driver **out,
+ git_diff_driver_registry *reg,
+ const char *driver_name)
+{
+ git_diff_driver_definition *ddef = NULL;
+ git_diff_driver *drv = NULL;
+ int error = 0;
+ size_t idx;
+
+ for (idx = 0; idx < ARRAY_SIZE(builtin_defs); ++idx) {
+ if (!strcasecmp(driver_name, builtin_defs[idx].name)) {
+ ddef = &builtin_defs[idx];
+ break;
+ }
+ }
+ if (!ddef)
+ goto done;
+
+ if ((error = diff_driver_alloc(&drv, NULL, ddef->name)) < 0)
+ goto done;
+
+ drv->type = DIFF_DRIVER_PATTERNLIST;
+
+ if (ddef->fns &&
+ (error = diff_driver_add_patterns(
+ drv, ddef->fns, ddef->flags)) < 0)
+ goto done;
+
+ if (ddef->words &&
+ (error = git_regexp_compile(&drv->word_pattern, ddef->words, ddef->flags)) < 0)
+ goto done;
+
+ if ((error = git_strmap_set(reg->drivers, drv->name, drv)) < 0)
+ goto done;
+
+done:
+ if (error && drv)
+ git_diff_driver_free(drv);
+ else
+ *out = drv;
+
+ return error;
+}
+
+static int git_diff_driver_load(
+ git_diff_driver **out, git_repository *repo, const char *driver_name)
+{
+ int error = 0;
+ git_diff_driver_registry *reg;
+ git_diff_driver *drv;
+ size_t namelen;
+ git_config *cfg = NULL;
+ git_str name = GIT_STR_INIT;
+ git_config_entry *ce = NULL;
+ bool found_driver = false;
+
+ if ((reg = git_repository_driver_registry(repo)) == NULL)
+ return -1;
+
+ if ((drv = git_strmap_get(reg->drivers, driver_name)) != NULL) {
+ *out = drv;
+ return 0;
+ }
+
+ if ((error = diff_driver_alloc(&drv, &namelen, driver_name)) < 0)
+ goto done;
+
+ drv->type = DIFF_DRIVER_AUTO;
+
+ /* if you can't read config for repo, just use default driver */
+ if (git_repository_config_snapshot(&cfg, repo) < 0) {
+ git_error_clear();
+ goto done;
+ }
+
+ if ((error = git_str_printf(&name, "diff.%s.binary", driver_name)) < 0)
+ goto done;
+
+ switch (git_config__get_bool_force(cfg, name.ptr, -1)) {
+ case true:
+ /* if diff.<driver>.binary is true, just return the binary driver */
+ *out = &diff_driver_binary;
+ goto done;
+ case false:
+ /* if diff.<driver>.binary is false, force binary checks off */
+ /* but still may have custom function context patterns, etc. */
+ drv->binary_flags = GIT_DIFF_FORCE_TEXT;
+ found_driver = true;
+ break;
+ default:
+ /* diff.<driver>.binary unspecified or "auto", so just continue */
+ break;
+ }
+
+ /* TODO: warn if diff.<name>.command or diff.<name>.textconv are set */
+
+ git_str_truncate(&name, namelen + strlen("diff.."));
+ if ((error = git_str_PUTS(&name, "xfuncname")) < 0)
+ goto done;
+
+ if ((error = git_config_get_multivar_foreach(
+ cfg, name.ptr, NULL, diff_driver_xfuncname, drv)) < 0) {
+ if (error != GIT_ENOTFOUND)
+ goto done;
+ git_error_clear(); /* no diff.<driver>.xfuncname, so just continue */
+ }
+
+ git_str_truncate(&name, namelen + strlen("diff.."));
+ if ((error = git_str_PUTS(&name, "funcname")) < 0)
+ goto done;
+
+ if ((error = git_config_get_multivar_foreach(
+ cfg, name.ptr, NULL, diff_driver_funcname, drv)) < 0) {
+ if (error != GIT_ENOTFOUND)
+ goto done;
+ git_error_clear(); /* no diff.<driver>.funcname, so just continue */
+ }
+
+ /* if we found any patterns, set driver type to use correct callback */
+ if (git_array_size(drv->fn_patterns) > 0) {
+ drv->type = DIFF_DRIVER_PATTERNLIST;
+ found_driver = true;
+ }
+
+ git_str_truncate(&name, namelen + strlen("diff.."));
+ if ((error = git_str_PUTS(&name, "wordregex")) < 0)
+ goto done;
+
+ if ((error = git_config__lookup_entry(&ce, cfg, name.ptr, false)) < 0)
+ goto done;
+ if (!ce || !ce->value)
+ /* no diff.<driver>.wordregex, so just continue */;
+ else if (!(error = git_regexp_compile(&drv->word_pattern, ce->value, 0)))
+ found_driver = true;
+ else {
+ /* TODO: warn about bad regex instead of failure */
+ goto done;
+ }
+
+ /* TODO: look up diff.<driver>.algorithm to turn on minimal / patience
+ * diff in drv->other_flags
+ */
+
+ /* if no driver config found at all, fall back on AUTO driver */
+ if (!found_driver)
+ goto done;
+
+ /* store driver in registry */
+ if ((error = git_strmap_set(reg->drivers, drv->name, drv)) < 0)
+ goto done;
+
+ *out = drv;
+
+done:
+ git_config_entry_free(ce);
+ git_str_dispose(&name);
+ git_config_free(cfg);
+
+ if (!*out) {
+ int error2 = git_diff_driver_builtin(out, reg, driver_name);
+ if (!error)
+ error = error2;
+ }
+
+ if (drv && drv != *out)
+ git_diff_driver_free(drv);
+
+ return error;
+}
+
+int git_diff_driver_lookup(
+ git_diff_driver **out, git_repository *repo,
+ git_attr_session *attrsession, const char *path)
+{
+ int error = 0;
+ const char *values[1], *attrs[] = { "diff" };
+
+ GIT_ASSERT_ARG(out);
+ *out = NULL;
+
+ if (!repo || !path || !strlen(path))
+ /* just use the auto value */;
+ else if ((error = git_attr_get_many_with_session(values, repo,
+ attrsession, 0, path, 1, attrs)) < 0)
+ /* return error below */;
+
+ else if (GIT_ATTR_IS_UNSPECIFIED(values[0]))
+ /* just use the auto value */;
+ else if (GIT_ATTR_IS_FALSE(values[0]))
+ *out = &diff_driver_binary;
+ else if (GIT_ATTR_IS_TRUE(values[0]))
+ *out = &diff_driver_text;
+
+ /* otherwise look for driver information in config and build driver */
+ else if ((error = git_diff_driver_load(out, repo, values[0])) < 0) {
+ if (error == GIT_ENOTFOUND) {
+ error = 0;
+ git_error_clear();
+ }
+ }
+
+ if (!*out)
+ *out = &diff_driver_auto;
+
+ return error;
+}
+
+void git_diff_driver_free(git_diff_driver *driver)
+{
+ git_diff_driver_pattern *pat;
+
+ if (!driver)
+ return;
+
+ while ((pat = git_array_pop(driver->fn_patterns)) != NULL)
+ git_regexp_dispose(&pat->re);
+ git_array_clear(driver->fn_patterns);
+
+ git_regexp_dispose(&driver->word_pattern);
+
+ git__free(driver);
+}
+
+void git_diff_driver_update_options(
+ uint32_t *option_flags, git_diff_driver *driver)
+{
+ if ((*option_flags & FORCE_DIFFABLE) == 0)
+ *option_flags |= driver->binary_flags;
+
+ *option_flags |= driver->other_flags;
+}
+
+int git_diff_driver_content_is_binary(
+ git_diff_driver *driver, const char *content, size_t content_len)
+{
+ git_str search = GIT_STR_INIT;
+
+ GIT_UNUSED(driver);
+
+ git_str_attach_notowned(&search, content,
+ min(content_len, GIT_FILTER_BYTES_TO_CHECK_NUL));
+
+ /* TODO: provide encoding / binary detection callbacks that can
+ * be UTF-8 aware, etc. For now, instead of trying to be smart,
+ * let's just use the simple NUL-byte detection that core git uses.
+ */
+
+ /* previously was: if (git_str_is_binary(&search)) */
+ if (git_str_contains_nul(&search))
+ return 1;
+
+ return 0;
+}
+
+static int diff_context_line__simple(
+ git_diff_driver *driver, git_str *line)
+{
+ char firstch = line->ptr[0];
+ GIT_UNUSED(driver);
+ return (git__isalpha(firstch) || firstch == '_' || firstch == '$');
+}
+
+static int diff_context_line__pattern_match(
+ git_diff_driver *driver, git_str *line)
+{
+ size_t i, maxi = git_array_size(driver->fn_patterns);
+ git_regmatch pmatch[2];
+
+ for (i = 0; i < maxi; ++i) {
+ git_diff_driver_pattern *pat = git_array_get(driver->fn_patterns, i);
+
+ if (!git_regexp_search(&pat->re, line->ptr, 2, pmatch)) {
+ if (pat->flags & REG_NEGATE)
+ return false;
+
+ /* use pmatch data to trim line data */
+ i = (pmatch[1].start >= 0) ? 1 : 0;
+ git_str_consume(line, git_str_cstr(line) + pmatch[i].start);
+ git_str_truncate(line, pmatch[i].end - pmatch[i].start);
+ git_str_rtrim(line);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static long diff_context_find(
+ const char *line,
+ long line_len,
+ char *out,
+ long out_size,
+ void *payload)
+{
+ git_diff_find_context_payload *ctxt = payload;
+
+ if (git_str_set(&ctxt->line, line, (size_t)line_len) < 0)
+ return -1;
+ git_str_rtrim(&ctxt->line);
+
+ if (!ctxt->line.size)
+ return -1;
+
+ if (!ctxt->match_line || !ctxt->match_line(ctxt->driver, &ctxt->line))
+ return -1;
+
+ if (out_size > (long)ctxt->line.size)
+ out_size = (long)ctxt->line.size;
+ memcpy(out, ctxt->line.ptr, (size_t)out_size);
+
+ return out_size;
+}
+
+void git_diff_find_context_init(
+ git_diff_find_context_fn *findfn_out,
+ git_diff_find_context_payload *payload_out,
+ git_diff_driver *driver)
+{
+ *findfn_out = driver ? diff_context_find : NULL;
+
+ memset(payload_out, 0, sizeof(*payload_out));
+ if (driver) {
+ payload_out->driver = driver;
+ payload_out->match_line = (driver->type == DIFF_DRIVER_PATTERNLIST) ?
+ diff_context_line__pattern_match : diff_context_line__simple;
+ git_str_init(&payload_out->line, 0);
+ }
+}
+
+void git_diff_find_context_clear(git_diff_find_context_payload *payload)
+{
+ if (payload) {
+ git_str_dispose(&payload->line);
+ payload->driver = NULL;
+ }
+}