UnifiedDiffFormatter.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.git;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;

import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.HistogramDiff;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.util.QuotedString;

/**
 * Formats text changes as a unified diff.
 */
public final class UnifiedDiffFormatter {

	private static final int CONTEXT_LINES= 3;

	private UnifiedDiffFormatter() {
		// Utility class.
	}

	/**
	 * Formats the given file content change as a unified diff.
	 *
	 * @param relativePath the project-relative file path
	 * @param before the original file bytes
	 * @param after the updated file bytes
	 * @return the unified diff bytes, or an empty array when the content is unchanged
	 * @throws IOException if formatting fails
	 */
	public static byte[] format(String relativePath, byte[] before, byte[] after) throws IOException {
		Objects.requireNonNull(relativePath, "relativePath"); //$NON-NLS-1$
		if (relativePath.isBlank()) {
			throw new IllegalArgumentException("relativePath must not be blank"); //$NON-NLS-1$
		}
		byte[] original= before == null ? new byte[0] : before.clone();
		byte[] updated= after == null ? new byte[0] : after.clone();
		if (Arrays.equals(original, updated)) {
			return new byte[0];
		}
		RawText beforeText= new RawText(original);
		RawText afterText= new RawText(updated);
		EditList edits= new HistogramDiff().diff(RawTextComparator.DEFAULT, beforeText, afterText);
		if (edits.isEmpty()) {
			return new byte[0];
		}
		ByteArrayOutputStream output= new ByteArrayOutputStream();
		writeHeader(output, relativePath);
		try (DiffFormatter formatter= new DiffFormatter(output)) {
			formatter.setContext(CONTEXT_LINES);
			formatter.format(edits, beforeText, afterText);
			formatter.flush();
		}
		return output.toByteArray();
	}

	private static void writeHeader(ByteArrayOutputStream output, String relativePath) {
		output.writeBytes(("--- " + QuotedString.GIT_PATH.quote("a/" + relativePath) + '\n') //$NON-NLS-1$ //$NON-NLS-2$
				.getBytes(StandardCharsets.UTF_8));
		output.writeBytes(("+++ " + QuotedString.GIT_PATH.quote("b/" + relativePath) + '\n') //$NON-NLS-1$ //$NON-NLS-2$
				.getBytes(StandardCharsets.UTF_8));
	}
}