diff_xdiff.c 7.35 KB
Newer Older
1 2 3 4 5 6
/*
 * 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.
 */
7 8

#include "diff_xdiff.h"
9
#include "util.h"
10

11
#include "git2/errors.h"
12 13
#include "diff.h"
#include "diff_driver.h"
14
#include "patch_generate.h"
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

static int git_xdiff_scan_int(const char **str, int *value)
{
	const char *scan = *str;
	int v = 0, digits = 0;
	/* find next digit */
	for (scan = *str; *scan && !git__isdigit(*scan); scan++);
	/* parse next number */
	for (; git__isdigit(*scan); scan++, digits++)
		v = (v * 10) + (*scan - '0');
	*str = scan;
	*value = v;
	return (digits > 0) ? 0 : -1;
}

30
static int git_xdiff_parse_hunk(git_diff_hunk *hunk, const char *header)
31 32 33
{
	/* expect something of the form "@@ -%d[,%d] +%d[,%d] @@" */
	if (*header != '@')
34
		goto fail;
35
	if (git_xdiff_scan_int(&header, &hunk->old_start) < 0)
36
		goto fail;
37
	if (*header == ',') {
38
		if (git_xdiff_scan_int(&header, &hunk->old_lines) < 0)
39
			goto fail;
40
	} else
41 42
		hunk->old_lines = 1;
	if (git_xdiff_scan_int(&header, &hunk->new_start) < 0)
43
		goto fail;
44
	if (*header == ',') {
45
		if (git_xdiff_scan_int(&header, &hunk->new_lines) < 0)
46
			goto fail;
47
	} else
48 49
		hunk->new_lines = 1;
	if (hunk->old_start < 0 || hunk->new_start < 0)
50
		goto fail;
51 52

	return 0;
53 54

fail:
55
	git_error_set(GIT_ERROR_INVALID, "malformed hunk header from xdiff");
56
	return -1;
57 58 59 60
}

typedef struct {
	git_xdiff_output *xo;
61
	git_patch_generated *patch;
62
	git_diff_hunk hunk;
Russell Belfer committed
63
	int old_lineno, new_lineno;
64
	mmfile_t xd_old_data, xd_new_data;
65 66
} git_xdiff_info;

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
static int diff_update_lines(
	git_xdiff_info *info,
	git_diff_line *line,
	const char *content,
	size_t content_len)
{
	const char *scan = content, *scan_end = content + content_len;

	for (line->num_lines = 0; scan < scan_end; ++scan)
		if (*scan == '\n')
			++line->num_lines;

	line->content     = content;
	line->content_len = content_len;

	/* expect " "/"-"/"+", then data */
	switch (line->origin) {
	case GIT_DIFF_LINE_ADDITION:
	case GIT_DIFF_LINE_DEL_EOFNL:
		line->old_lineno = -1;
		line->new_lineno = info->new_lineno;
Russell Belfer committed
88
		info->new_lineno += (int)line->num_lines;
89 90 91 92 93
		break;
	case GIT_DIFF_LINE_DELETION:
	case GIT_DIFF_LINE_ADD_EOFNL:
		line->old_lineno = info->old_lineno;
		line->new_lineno = -1;
Russell Belfer committed
94
		info->old_lineno += (int)line->num_lines;
95 96 97 98 99
		break;
	case GIT_DIFF_LINE_CONTEXT:
	case GIT_DIFF_LINE_CONTEXT_EOFNL:
		line->old_lineno = info->old_lineno;
		line->new_lineno = info->new_lineno;
Russell Belfer committed
100 101
		info->old_lineno += (int)line->num_lines;
		info->new_lineno += (int)line->num_lines;
102 103
		break;
	default:
104
		git_error_set(GIT_ERROR_INVALID, "unknown diff line origin %02x",
105 106 107 108 109 110 111
			(unsigned int)line->origin);
		return -1;
	}

	return 0;
}

112 113 114
static int git_xdiff_cb(void *priv, mmbuffer_t *bufs, int len)
{
	git_xdiff_info *info = priv;
115
	git_patch_generated *patch = info->patch;
116
	const git_diff_delta *delta = patch->base.delta;
117
	git_patch_generated_output *output = &info->xo->output;
118
	git_diff_line line;
119
	size_t buffer_len;
120 121

	if (len == 1) {
122
		output->error = git_xdiff_parse_hunk(&info->hunk, bufs[0].ptr);
123 124 125
		if (output->error < 0)
			return output->error;

126 127 128
		info->hunk.header_len = bufs[0].size;
		if (info->hunk.header_len >= sizeof(info->hunk.header))
			info->hunk.header_len = sizeof(info->hunk.header) - 1;
129 130 131 132 133 134 135 136 137 138

		/* Sanitize the hunk header in case there is invalid Unicode */
		buffer_len = git__utf8_valid_buf_length((const uint8_t *) bufs[0].ptr, info->hunk.header_len);
		/* Sanitizing the hunk header may delete the newline, so add it back again if there is room */
		if (buffer_len < info->hunk.header_len) {
			bufs[0].ptr[buffer_len] = '\n';
			buffer_len += 1;
			info->hunk.header_len = buffer_len;
		}

139 140 141
		memcpy(info->hunk.header, bufs[0].ptr, info->hunk.header_len);
		info->hunk.header[info->hunk.header_len] = '\0';

142
		if (output->hunk_cb != NULL &&
143 144 145
			(output->error = output->hunk_cb(
				delta, &info->hunk, output->payload)))
			return output->error;
146 147 148

		info->old_lineno = info->hunk.old_start;
		info->new_lineno = info->hunk.new_start;
149 150 151 152
	}

	if (len == 2 || len == 3) {
		/* expect " "/"-"/"+", then data */
153
		line.origin =
154 155 156 157
			(*bufs[0].ptr == '+') ? GIT_DIFF_LINE_ADDITION :
			(*bufs[0].ptr == '-') ? GIT_DIFF_LINE_DELETION :
			GIT_DIFF_LINE_CONTEXT;

158 159 160 161 162 163 164
		if (line.origin == GIT_DIFF_LINE_ADDITION)
			line.content_offset = bufs[1].ptr - info->xd_new_data.ptr;
		else if (line.origin == GIT_DIFF_LINE_DELETION)
			line.content_offset = bufs[1].ptr - info->xd_old_data.ptr;
		else
			line.content_offset = -1;

165 166 167
		output->error = diff_update_lines(
			info, &line, bufs[1].ptr, bufs[1].size);

168 169 170
		if (!output->error && output->data_cb != NULL)
			output->error = output->data_cb(
				delta, &info->hunk, &line, output->payload);
171 172 173 174 175 176 177 178
	}

	if (len == 3 && !output->error) {
		/* If we have a '+' and a third buf, then we have added a line
		 * without a newline and the old code had one, so DEL_EOFNL.
		 * If we have a '-' and a third buf, then we have removed a line
		 * with out a newline but added a blank line, so ADD_EOFNL.
		 */
179
		line.origin =
180 181 182 183
			(*bufs[0].ptr == '+') ? GIT_DIFF_LINE_DEL_EOFNL :
			(*bufs[0].ptr == '-') ? GIT_DIFF_LINE_ADD_EOFNL :
			GIT_DIFF_LINE_CONTEXT_EOFNL;

184 185
		line.content_offset = -1;

186 187 188
		output->error = diff_update_lines(
			info, &line, bufs[2].ptr, bufs[2].size);

189 190 191
		if (!output->error && output->data_cb != NULL)
			output->error = output->data_cb(
				delta, &info->hunk, &line, output->payload);
192 193 194 195 196
	}

	return output->error;
}

197
static int git_xdiff(git_patch_generated_output *output, git_patch_generated *patch)
198 199 200
{
	git_xdiff_output *xo = (git_xdiff_output *)output;
	git_xdiff_info info;
201
	git_diff_find_context_payload findctxt;
202 203 204 205 206 207 208

	memset(&info, 0, sizeof(info));
	info.patch = patch;
	info.xo    = xo;

	xo->callback.priv = &info;

209
	git_diff_find_context_init(
210
		&xo->config.find_func, &findctxt, git_patch_generated_driver(patch));
211
	xo->config.find_func_priv = &findctxt;
212 213 214 215 216 217

	if (xo->config.find_func != NULL)
		xo->config.flags |= XDL_EMIT_FUNCNAMES;
	else
		xo->config.flags &= ~XDL_EMIT_FUNCNAMES;

218 219 220
	/* TODO: check ofile.opts_flags to see if driver-specific per-file
	 * updates are needed to xo->params.flags
	 */
221

222 223
	git_patch_generated_old_data(&info.xd_old_data.ptr, &info.xd_old_data.size, patch);
	git_patch_generated_new_data(&info.xd_new_data.ptr, &info.xd_new_data.size, patch);
224

225 226
	if (info.xd_old_data.size > GIT_XDIFF_MAX_SIZE ||
		info.xd_new_data.size > GIT_XDIFF_MAX_SIZE) {
227
		git_error_set(GIT_ERROR_INVALID, "files too large for diff");
228 229 230
		return -1;
	}

231
	xdl_diff(&info.xd_old_data, &info.xd_new_data,
232 233
		&xo->params, &xo->config, &xo->callback);

234 235
	git_diff_find_context_clear(&findctxt);

236 237 238 239 240
	return xo->output.error;
}

void git_xdiff_init(git_xdiff_output *xo, const git_diff_options *opts)
{
Russell Belfer committed
241
	uint32_t flags = opts ? opts->flags : 0;
242 243 244 245 246 247 248 249 250 251 252 253

	xo->output.diff_cb = git_xdiff;

	xo->config.ctxlen = opts ? opts->context_lines : 3;
	xo->config.interhunkctxlen = opts ? opts->interhunk_lines : 0;

	if (flags & GIT_DIFF_IGNORE_WHITESPACE)
		xo->params.flags |= XDF_WHITESPACE_FLAGS;
	if (flags & GIT_DIFF_IGNORE_WHITESPACE_CHANGE)
		xo->params.flags |= XDF_IGNORE_WHITESPACE_CHANGE;
	if (flags & GIT_DIFF_IGNORE_WHITESPACE_EOL)
		xo->params.flags |= XDF_IGNORE_WHITESPACE_AT_EOL;
254 255
	if (flags & GIT_DIFF_INDENT_HEURISTIC)
		xo->params.flags |= XDF_INDENT_HEURISTIC;
256

257 258 259 260 261
	if (flags & GIT_DIFF_PATIENCE)
		xo->params.flags |= XDF_PATIENCE_DIFF;
	if (flags & GIT_DIFF_MINIMAL)
		xo->params.flags |= XDF_NEED_MINIMAL;

262 263
	xo->callback.outf = git_xdiff_cb;
}