blame.cache: Define interface and POJO to cache blame results

The blame generator goes into the history of a file until it blames
all lines. If the caller keeps a cache of the results, the generator
could use it to shorten that walk, using the blame information of an
older revision of the file to build the new blame view.

Define an interface and POJO for the blame cache.

Change-Id: Ib6b033ef46089bbc5a5b32e8e060d4ab0f74b871
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java
new file mode 100644
index 0000000..d44fb5f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/BlameCache.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.blame.cache;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Keeps the blame information for a path at certain commit.
+ * <p>
+ * If there is a result, it covers the whole file at that revision
+ *
+ * @since 7.2
+ */
+public interface BlameCache {
+	/**
+	 * Gets the blame of a path at a given commit if available.
+	 * <p>
+	 * Since this cache is used in blame calculation, this get() method should
+	 * only retrieve the cache value, and not re-trigger blame calculation. In
+	 * other words, this acts as "getIfPresent", and not "computeIfAbsent".
+	 *
+	 * @param repo
+	 *            repository containing the commit
+	 * @param commitId
+	 *            we are looking at the file in this revision
+	 * @param path
+	 *            path a file in the repo
+	 *
+	 * @return the blame of a path at a given commit or null if not in cache
+	 * @throws IOException
+	 *             error retrieving/parsing values from storage
+	 */
+	List<CacheRegion> get(Repository repo, ObjectId commitId, String path)
+			throws IOException;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java
new file mode 100644
index 0000000..6aa4eef
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/cache/CacheRegion.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.blame.cache;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Region of the blame of a file.
+ * <p>
+ * Usually all parameters are non-null, except when the Region was created
+ * to fill an unblamed gap (to cover for bugs in the calculation). In that
+ * case, path, commit and author will be null.
+ *
+ * @since 7.2
+ **/
+public class CacheRegion implements Comparable<CacheRegion> {
+	private final String sourcePath;
+
+	private final ObjectId sourceCommit;
+
+	private final int end;
+
+	private final int start;
+
+	/**
+	 * A blamed portion of a file
+	 *
+	 * @param path
+	 *            location of the file
+	 * @param commit
+	 *            commit that is modifying this region
+	 * @param start
+	 *            first line of this region (inclusive)
+	 * @param end
+	 *            last line of this region (non-inclusive!)
+	 */
+	public CacheRegion(String path, ObjectId commit,
+			int start, int end) {
+		allOrNoneNull(path, commit);
+		this.sourcePath = path;
+		this.sourceCommit = commit;
+		this.start = start;
+		this.end = end;
+	}
+
+	/**
+	 * First line of this region. Starting by 0, inclusive
+	 *
+	 * @return first line of this region.
+	 */
+	public int getStart() {
+		return start;
+	}
+
+	/**
+	 * One after last line in this region (or: last line non-inclusive)
+	 *
+	 * @return one after last line in this region.
+	 */
+	public int getEnd() {
+		return end;
+	}
+
+
+	/**
+	 * Path of the file this region belongs to
+	 *
+	 * @return path in the repo/commit
+	 */
+	public String getSourcePath() {
+		return sourcePath;
+	}
+
+	/**
+	 * Commit this region belongs to
+	 *
+	 * @return commit for this region
+	 */
+	public ObjectId getSourceCommit() {
+		return sourceCommit;
+	}
+
+	@Override
+	public int compareTo(CacheRegion o) {
+		return start - o.start;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder sb = new StringBuilder();
+		if (sourceCommit != null) {
+			sb.append(sourceCommit.name(), 0, 7).append(' ')
+					.append(" (")
+					.append(sourcePath).append(')');
+		} else {
+			sb.append("<unblamed region>");
+		}
+		sb.append(' ').append("start=").append(start).append(", count=")
+				.append(end - start);
+		return sb.toString();
+	}
+
+	private static void allOrNoneNull(String path, ObjectId commit) {
+		if (path != null && commit != null) {
+			return;
+		}
+
+		if (path == null && commit == null) {
+			return;
+		}
+		throw new IllegalArgumentException(String.format(
+				"expected all null or none: %s, %s", path, commit));
+	}
+}