/* * Copyright 2016 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.uberfire.java.nio.fs.jgit.util.commands; import java.io.IOException; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Spliterator; import java.util.stream.StreamSupport; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.uberfire.java.nio.fs.jgit.util.JGitUtil; import org.uberfire.java.nio.fs.jgit.util.exceptions.ConcurrentRefUpdateException; import org.uberfire.java.nio.fs.jgit.util.exceptions.GitException; import static org.eclipse.jgit.lib.Constants.HEAD; /** * Implements the Git Squash command. It needs the repository were he is going to make the squash, * the squash commit message, and the start commit, to know from where he has to squash. * It return an Empty Optional because is not necessary to return anything. * It throws a {@link GitException} if something bad happens. */ public class Squash extends GitCommand { private final String branch; private final Git git; private String squashedCommitMessage; private String startCommitString; public Squash(final Git git, final String branch, final String startCommitString, final String squashedCommitMessage) { this.git = git; this.squashedCommitMessage = squashedCommitMessage; this.branch = branch; this.startCommitString = startCommitString; } public Optional<Void> execute() { final Repository repo = this.git.getRepository(); this.isBare(repo); this.checkIfCommitIsPresentAtBranch(this.git, this.branch, this.startCommitString); final Git git = new Git(repo); ObjectId startCommitObjectId = this.getStartCommit(git, startCommitString); RevWalk revWalk = new RevWalk(repo); RevCommit startCommit = getRevCommit(startCommitObjectId, revWalk); RevCommit parent = startCommit; if (startCommit.getParentCount() > 0) { parent = getRevCommit(startCommitObjectId, revWalk).getParent(0); } Ref head = this.getHead(repo); this.markStart(revWalk, parent, head); revWalk.sort(RevSort.REVERSE); PersonIdent commitAuthor = null; Map<String, ObjectId> content = new HashMap<String, ObjectId>(); for (RevCommit commit : revWalk) { commitAuthor = commit.getAuthorIdent(); content = collectPathAndObjectIdFromTree(repo, revWalk, commit); } revWalk.dispose(); final ObjectInserter odi = repo.newObjectInserter(); final ObjectId indexTreeId = createTemporaryIndex(git, content, odi); final CommitBuilder commit = createCommit(parent, commitAuthor, indexTreeId, this.squashedCommitMessage); final ObjectId commitId = insertCommitIntoRepositoryAndFlush(odi, commit); updateReferenceAndReleaseRevisionWalk(git, revWalk, commitId); return Optional.empty(); } /** * It checks if the commit is present on branch logs. If not it throws a {@link GitException} * @param git The git repository * @param branch The branch where it is going to do the search * @param startCommitString The commit it needs to find * @throws {@link GitException} when it cannot find the commit in that branch */ private void checkIfCommitIsPresentAtBranch(final Git git, final String branch, final String startCommitString) { try { final ObjectId id = JGitUtil.resolveObjectId(git, branch); final Spliterator<RevCommit> log = git.log().add(id).call().spliterator(); final Optional<RevCommit> result = StreamSupport.stream(log, false) .filter((elem) -> elem.getName().equals(startCommitString)) .findFirst(); result.orElseThrow(() -> new GitException("Commit is not present at branch " + branch)); } catch (GitAPIException | MissingObjectException | IncorrectObjectTypeException e) { throw new GitException("A problem occurred when trying to get commit list" + "", e); } } /** * Create temporary index for commit content * @param git the git repository * @param content the content for the temporary index * @param odi the object inserter * @return the object id with the temporary index reference. * @throws {@link GitException} if cannot create temporary Index */ private ObjectId createTemporaryIndex(Git git, Map<String, ObjectId> content, ObjectInserter odi) { try { DirCache index = JGitUtil.createTemporaryIndexForContent(git, content); return index.writeTree(odi); } catch (IOException e) { String message = "Cannot create temporary index form content"; throw new GitException(message, e); } } /** * Update the reference of the old commit with the new squashed commits. * @param git The git Respository * @param revWalk the object that walks into the commit graph. * @param commitId the Commit Id that contains the reference. * @throws {@link ConcurrentRefUpdateException} if cannot lock head. * @throws {@link JGitInternalException} if updating ref failed. * @throws {@link GitException} if cannot update the commit reference. */ private void updateReferenceAndReleaseRevisionWalk(Git git, RevWalk revWalk, ObjectId commitId) { try { final RevCommit revCommit = getRevCommit(commitId, revWalk); final RefUpdate ru = git.getRepository().updateRef(getBranch()); ru.setExpectedOldObjectId(git.getRepository().resolve(HEAD)); ru.setNewObjectId(commitId); ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); final RefUpdate.Result rc = ru.forceUpdate(); switch (rc) { case NEW: case FORCED: case FAST_FORWARD: break; case REJECTED: case LOCK_FAILURE: throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, ru.getRef(), rc); default: throw new JGitInternalException(MessageFormat.format(JGitText.get().updatingRefFailed, Constants.HEAD, commitId.toString(), rc)); } } catch (IOException e) { String message = "Cannot update commit reference"; throw new GitException(message, e); } finally { revWalk.close(); } } private String getBranch() { return "refs/heads/" + this.branch; } /** * Collect all paths and object IDs from Git Tree * @param repo the repository * @param revWalk the object that walks into the commit graph. * @param commit the commit * @return a Map where the key is the path and the values is the object id * @throws {@link GitException} if something wrong happens */ private Map<String, ObjectId> collectPathAndObjectIdFromTree(Repository repo, RevWalk revWalk, RevCommit commit) { try { Map<String, ObjectId> content = new HashMap<String, ObjectId>(); RevTree tree = this.getRevTree(revWalk, commit); TreeWalk treeWalk = new TreeWalk(repo); treeWalk.addTree(tree); treeWalk.setRecursive(false); while (treeWalk.next()) { if (treeWalk.isSubtree()) { treeWalk.enterSubtree(); } else { ObjectId objectId = treeWalk.getObjectId(0); content.put(treeWalk.getPathString(), objectId); } } return content; } catch (IOException e) { String message = "Impossible to collect path and objectId from Tree"; throw new GitException(message, e); } } /** * Mark commits to start graph traversal from and mark the * commit as Untinteresting to not produce in the output. * @param revWalk the object that walks into the commit graph. * @param parent the parent commit. * @param head the HEAD reference of the repository. */ private void markStart(RevWalk revWalk, RevCommit parent, Ref head) { try { revWalk.markStart(getRevCommit(head.getObjectId(), revWalk)); revWalk.markUninteresting(parent); } catch (IOException e) { String message = "Cannot mark start a revision tree"; throw new GitException(message, e); } } /** * Get HEAD from Git Repository * @param repository the repository where to find the HEAD * @return The HEAD Reference * @throws {@link GitException} if cannot get HEAD */ private Ref getHead(Repository repository) { try { return repository.getRef(HEAD); } catch (IOException e) { String message = "Cannot get HEAD from Repository"; throw new GitException(message, e); } } /** * Insert commits into resporitory * @param odi object that inserts commits into tree * @param commit the commit to insert * @return Return the commit id inserted. */ private ObjectId insertCommitIntoRepositoryAndFlush(ObjectInserter odi, CommitBuilder commit) { try { final ObjectId commitId = odi.insert(commit); odi.flush(); return commitId; } catch (IOException e) { String message = String.format("Cannot get insert commits into repository (TreeId = %s)", commit.getTreeId()); throw new GitException(message, e); } } /** * Just extracts the behaviour to create a CommmitBuilder into this method. * @param parent Commit * @param commitAuthor The commit author * @param indexTreeId the index of the tree where the commit belongs. * @param squashedCommitMessage the message for the commit. * @return the with all the parameters applied. */ private CommitBuilder createCommit(RevCommit parent, PersonIdent commitAuthor, ObjectId indexTreeId, String squashedCommitMessage) { final CommitBuilder commit = new CommitBuilder(); commit.setAuthor(commitAuthor); commit.setCommitter(commitAuthor); commit.setEncoding(Constants.CHARACTER_ENCODING); commit.setMessage(squashedCommitMessage); commit.setParentId(parent.getId()); commit.setTreeId(indexTreeId); return commit; } }