org.openlca.git.writer.CommitWriter Maven / Gradle / Ivy
The newest version!
package org.openlca.git.writer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.List;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.storage.file.PackInserter;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.openlca.git.Compatibility;
import org.openlca.git.RepositoryInfo;
import org.openlca.git.iterator.ChangeIterator;
import org.openlca.git.iterator.EntryIterator;
import org.openlca.git.model.Change;
import org.openlca.git.model.Change.ChangeType;
import org.openlca.git.repo.OlcaRepository;
import org.openlca.git.util.BinaryResolver;
import org.openlca.git.util.GitUtil;
import org.openlca.git.util.ProgressMonitor;
import org.openlca.jsonld.LibraryLink;
import org.openlca.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class CommitWriter {
private static final Logger log = LoggerFactory.getLogger(CommitWriter.class);
protected final OlcaRepository repo;
protected final BinaryResolver binaryResolver;
protected String ref = Constants.HEAD;
protected PersonIdent committer = new PersonIdent("anonymous", "[email protected]");
protected ProgressMonitor progressMonitor = ProgressMonitor.NULL;
protected UsedFeatures usedFeatures;
private PackInserter packInserter;
private ObjectInserter objectInserter;
public CommitWriter(OlcaRepository repo, BinaryResolver binaryResolver) {
this.repo = repo;
this.binaryResolver = binaryResolver;
}
public CommitWriter ref(String ref) {
this.ref = ref != null ? ref : Constants.HEAD;
return this;
}
public CommitWriter as(PersonIdent committer) {
this.committer = committer != null ? committer : new PersonIdent("anonymous", "[email protected]");
return this;
}
public CommitWriter with(ProgressMonitor progressMonitor) {
this.progressMonitor = progressMonitor != null ? progressMonitor : ProgressMonitor.NULL;
return this;
}
protected String write(String message, List changes, ObjectId... parentCommitIds) throws IOException {
return write(message, new ChangeIterator(repo, binaryResolver, changes), parentCommitIds);
}
protected String write(String message, ChangeIterator changeIterator, ObjectId... parentCommitIds)
throws IOException {
if (this.usedFeatures == null) {
this.usedFeatures = UsedFeatures.of(repo, parentCommitIds);
}
Compatibility.checkRepositoryClientVersion(repo);
try {
init();
var treeIds = getCommitTreeIds(parentCommitIds);
var treeId = syncTree("", changeIterator, treeIds);
if (progressMonitor.isCanceled())
return null;
var commitId = commit(message, treeId, parentCommitIds);
return commitId.name();
} finally {
cleanUp();
}
}
private ObjectId[] getCommitTreeIds(ObjectId[] commitIds) throws IOException {
if (commitIds == null || commitIds.length == 0)
return null;
var treeIds = new ObjectId[commitIds.length];
for (var i = 0; i < commitIds.length; i++) {
treeIds[i] = getCommitTreeId(commitIds[i]);
}
return treeIds;
}
private ObjectId getCommitTreeId(ObjectId commitId) throws IOException {
if (commitId == null || ObjectId.zeroId().equals(commitId))
return null;
var commit = repo.parseCommit(commitId);
if (commit == null)
return null;
return commit.getTree().getId();
}
private void init() {
var firstCommit = repo.getHeadCommit() == null;
packInserter = repo.getObjectDatabase().newPackInserter();
packInserter.checkExisting(!firstCommit);
objectInserter = repo.newObjectInserter();
}
private ObjectId syncTree(String prefix, ChangeIterator iterator, ObjectId[] treeIds) {
var appended = false;
var tree = new TreeFormatter();
try (var walk = createWalk(prefix, iterator, treeIds)) {
var previous = "";
var previousWasDeleted = false;
while (walk.next()) {
if (progressMonitor.isCanceled())
return null;
var name = walk.getNameString();
if (name.equals(RepositoryInfo.FILE_NAME)) {
appendRepositoryInfo(tree);
continue;
}
if (previousWasDeleted && GitUtil.isBinDirOf(name, previous))
continue;
previous = name;
previousWasDeleted = false;
var mode = walk.getFileMode();
ObjectId id = null;
if (mode == FileMode.TREE) {
id = handleTree(walk, iterator);
} else if (mode == FileMode.REGULAR_FILE) {
id = handleFile(walk);
}
if (id == null || id.equals(ObjectId.zeroId())) {
previousWasDeleted = true;
continue;
}
tree.append(name, mode, id);
appended = true;
}
} catch (Exception e) {
log.error("Error walking tree", e);
}
if (!appended && !Strings.nullOrEmpty(prefix))
return null;
try {
var newId = objectInserter.insert(tree);
return newId;
} catch (IOException e) {
log.error("Error inserting tree", e);
return null;
}
}
private TreeWalk createWalk(String prefix, ChangeIterator iterator, ObjectId[] treeIds) throws IOException {
var walk = new TreeWalk(repo);
if (treeIds != null) {
for (var treeId : treeIds) {
addTree(walk, prefix, treeId);
}
}
if (iterator != null) {
walk.addTree(iterator);
} else {
walk.addTree(new EmptyTreeIterator());
}
return walk;
}
private void addTree(TreeWalk walk, String prefix, ObjectId treeId)
throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException {
if (treeId == null || treeId.equals(ObjectId.zeroId())) {
walk.addTree(new EmptyTreeIterator());
} else if (Strings.nullOrEmpty(prefix)) {
walk.addTree(treeId);
} else {
walk.addTree(new CanonicalTreeParser(GitUtil.encode(prefix).getBytes(), walk.getObjectReader(), treeId));
}
}
private ObjectId handleTree(TreeWalk walk, ChangeIterator iterator) {
var treeCount = walk.getTreeCount();
var treeIds = new ObjectId[treeCount - 1];
for (var i = 0; i < treeCount - 1; i++) {
treeIds[i] = walk.getFileMode(i) != FileMode.MISSING ? walk.getObjectId(i) : null;
}
var hasChanged = walk.getFileMode(treeCount - 1) != FileMode.MISSING;
if (hasChanged && isDeletedCategory(iterator.getEntryData()))
return null;
if (!hasChanged && treeCount == 2)
return treeIds[0];
var subIterator = hasChanged
? iterator.createSubtreeIterator()
: null;
var prefix = GitUtil.decode(walk.getPathString());
return syncTree(prefix, subIterator, treeIds);
}
private boolean isDeletedCategory(Change data) {
return data != null && data.isCategory && data.changeType == ChangeType.DELETE;
}
private ObjectId handleFile(TreeWalk walk)
throws IOException, InterruptedException {
var treeCount = walk.getTreeCount();
if (walk.getFileMode(treeCount - 1) == FileMode.MISSING) {
// second last commit is the remote commit in case of merge
// if no conflict resolution change is provided keep the remote one
for (var i = treeCount - 2; i >= 0; i--)
if (walk.getFileMode(i) != FileMode.MISSING)
return walk.getObjectId(i);
return null;
}
var path = GitUtil.decode(walk.getPathString());
var iterator = walk.getTree(treeCount - 1, EntryIterator.class);
Change change = iterator.getEntryData();
var filePath = iterator.getEntryFilePath();
if (change.changeType == ChangeType.DELETE && matches(path, change, filePath))
return null;
if (filePath != null)
return insertBlob(binaryResolver.resolve(change, filePath));
if (!change.isCategory) {
progressMonitor.subTask(change);
}
if (change.isCategory) {
usedFeatures.emptyCategories();
}
var data = change.isCategory
? new byte[0]
: getData(change);
if (data == null)
return null;
var blobId = insertBlob(data);
progressMonitor.worked(1);
return blobId;
}
private void appendRepositoryInfo(TreeFormatter tree) {
try {
var schemaBytes = usedFeatures.createInfo(getLibraries())
.json().toString().getBytes(StandardCharsets.UTF_8);
var blobId = insertBlob(schemaBytes);
if (blobId != null) {
tree.append(RepositoryInfo.FILE_NAME, FileMode.REGULAR_FILE, blobId);
}
} catch (Exception e) {
log.error("Error inserting repository info", e);
}
}
private ObjectId insertBlob(byte[] blob) throws IOException {
return packInserter.insert(Constants.OBJ_BLOB, blob);
}
private boolean matches(String path, Change change, String filePath) {
if (change == null)
return false;
if (filePath == null)
if (change.isCategory)
return path.equals(GitUtil.toEmptyCategoryPath(change.path));
else
return path.equals(change.path);
return GitUtil.isBinDirOrFileOf(path, change.path);
}
private ObjectId commit(String message, ObjectId treeId, ObjectId... parentIds) {
try {
var commit = new CommitBuilder();
commit.setAuthor(committer);
commit.setCommitter(committer);
commit.setMessage(message);
commit.setEncoding(StandardCharsets.UTF_8);
commit.setTreeId(treeId);
if (parentIds != null) {
for (var parentId : parentIds) {
commit.addParentId(parentId);
}
}
var commitId = objectInserter.insert(commit);
updateRef(message, commitId);
return commitId;
} catch (IOException e) {
log.error("failed to update head", e);
return null;
}
}
private void updateRef(String message, ObjectId commitId) throws IOException {
var update = repo.updateRef(ref);
update.setNewObjectId(commitId);
if (!Constants.R_STASH.equals(ref)) {
update.update();
} else {
update.setRefLogIdent(committer);
update.setRefLogMessage(message, false);
update.setForceRefLog(true);
update.forceUpdate();
}
}
protected void cleanUp() throws IOException {
if (packInserter != null) {
if (!progressMonitor.isCanceled()) {
packInserter.flush();
}
packInserter.close();
packInserter = null;
}
if (objectInserter != null) {
if (!progressMonitor.isCanceled()) {
objectInserter.flush();
}
objectInserter.close();
objectInserter = null;
}
if (!progressMonitor.isCanceled())
return;
try {
Git.wrap(repo).gc().setExpire(Calendar.getInstance().getTime()).call();
} catch (GitAPIException e) {
// ignore cleanup error
}
}
protected List getLibraries() {
return List.of();
}
protected abstract byte[] getData(Change change) throws IOException;
}