package br.uff.ic.dyevc.tools.vcs.git;
//~--- non-JDK imports --------------------------------------------------------
import br.uff.ic.dyevc.application.IConstants;
import br.uff.ic.dyevc.exception.DyeVCException;
import br.uff.ic.dyevc.exception.VCSException;
import br.uff.ic.dyevc.model.CommitChange;
import br.uff.ic.dyevc.model.CommitInfo;
import br.uff.ic.dyevc.model.CommitRelationship;
import br.uff.ic.dyevc.model.git.TrackedBranch;
import br.uff.ic.dyevc.model.MonitoredRepositories;
import br.uff.ic.dyevc.model.MonitoredRepository;
import br.uff.ic.dyevc.model.topology.RepositoryInfo;
import br.uff.ic.dyevc.persistence.CommitDAO;
import br.uff.ic.dyevc.utils.CommitInfoDateComparator;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.gitective.core.CommitUtils;
import org.slf4j.LoggerFactory;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* This class stores a history of commits in a Git repository.
*
* @author Cristiano
*/
public class GitCommitTools {
/**
* Map of commits associated that are pointed by any branch heads with the pointing branch names.
*/
private Map<String, List<String>> commitsToBranchNamesMap;
/**
* Map of commits associated that are pointed by any tags with the pointing tag names.
*/
private Map<String, List<String>> commitsToTagNamesMap;
/**
* Map of commits with its properties. Each commit is identified by its id.
*/
private Map<String, CommitInfo> commitInfoMap;
/**
* Map of commits found in tracked branches.
*/
private Map<String, CommitInfo> commitInfoTrackedMap;
/**
* Map of commits found in non-tracked branches.
*/
private Map<String, CommitInfo> commitInfoNonTrackedMap;
/**
* Map of commits not found in any of the repositories that {@link #rep} pushes from.
*/
Set<CommitInfo> notInPushListSet;
/**
* Map of commits not found in any of the repositories that {@link #rep} pulls to.
*/
Set<CommitInfo> notInPullListSet;
/**
* Map of commits not found locally in {@link #rep}.
*/
Set<CommitInfo> notInLocalRepositoryListSet;
/**
* Collection of commit relationships, relating a parent commit and its children.
*/
private List<CommitRelationship> commitRelationshipList;
/**
* Connector used by this class to connect to a Git repository.
*/
private GitConnector git;
private RevWalk walk;
/**
* The monitored repository used to instantiate this class, or null if not informed.
*/
private MonitoredRepository rep;
private boolean includeTopologyData;
private boolean initialized = false;
private static final int PARSED = 1 >> 0;
/**
* Creates a new instance of this class.
*
* @param rep The monitored repository to connect to.
* @param includeTopologyData If true, CommitInfo objects are filled with topology data
* @throws VCSException
*/
private GitCommitTools(MonitoredRepository rep, boolean includeTopologyData) throws VCSException {
this(rep.getConnection(), includeTopologyData);
this.rep = rep;
}
/**
* Creates a new instance of this class.
*
* @param git The connector to be used to connect to a Git repository.
* @param includeTopologyData If true, CommitInfo objects are filled with topology data
* @deprecated
*/
private GitCommitTools(GitConnector git, boolean includeTopologyData) {
this.git = git;
this.includeTopologyData = includeTopologyData;
}
/**
* Gets a new GitCommitTools instance.
*
* @param rep The monitored repository to connect to.
* @return An instance of GitCommitTools.
* @throws VCSException
*/
public static GitCommitTools getInstance(MonitoredRepository rep) throws VCSException {
return new GitCommitTools(rep, false);
}
/**
* Gets a new GitCommitTools instance.
*
* @param rep The monitored repository to connect to.
* @param includeTopologyData If true, CommitInfo objects are filled with topology data
* @return An instance of GitCommitTools.
* @throws VCSException
*/
public static GitCommitTools getInstance(MonitoredRepository rep, boolean includeTopologyData) throws VCSException {
return new GitCommitTools(rep, includeTopologyData);
}
/**
* Gets a new GitCommitTools instance.
*
* @param git the connector to be used to connect to a Git repository.
* @return An instance of GitCommitTools.
* @deprecated
*/
public static GitCommitTools getInstance(GitConnector git) {
return new GitCommitTools(git, false);
}
/**
* Sets the connection this instance must work with. Must be called before the class is initialized. Use it if you
* want to work with a connection to a different clone of the repository (e.g. the temporary working clone).
*
* @param connection A connection to a repository
*/
public void setConnection(GitConnector connection) throws VCSException {
if (initialized) {
throw new VCSException(
"Class was already instantiated and connection cannot be changed. Create another instance instead.");
}
this.git = connection;
}
/**
* The list of commitInfos is created in this method.
*
* @throws VCSException
*/
private void initialize() throws VCSException {
notInPushListSet = Collections.EMPTY_SET;
notInPullListSet = Collections.EMPTY_SET;
notInLocalRepositoryListSet = Collections.EMPTY_SET;
this.commitInfoMap = new TreeMap<String, CommitInfo>();
this.commitInfoTrackedMap = new TreeMap<String, CommitInfo>();
this.commitInfoNonTrackedMap = new TreeMap<String, CommitInfo>();
this.commitRelationshipList = new ArrayList<CommitRelationship>();
populateHistory();
initialized = true;
}
/**
* Returns the list of relationships between commits and its children.
*
* @return the list of commit relationships
* @throws VCSException
*/
public Collection<CommitRelationship> getCommitRelationships() throws VCSException {
if (!initialized) {
initialize();
}
return commitRelationshipList;
}
/**
* Returns the list of commits with its properties.
*
* @return the list of commits
* @throws VCSException
*/
public List<CommitInfo> getCommitInfos() throws VCSException {
if (!initialized) {
initialize();
}
List<CommitInfo> cis = new ArrayList<CommitInfo>(commitInfoMap.values());
Comparator<CommitInfo> comparator = new CommitInfoDateComparator();
Collections.sort(cis, comparator);
return (List)cis;
}
/**
* Returns a map with all commits, keyed by hash.
*
* @return a map with all commits, keyed by hash.
* @throws VCSException
*/
public Map<String, CommitInfo> getCommitInfoMap() throws VCSException {
if (!initialized) {
initialize();
}
return commitInfoMap;
}
/**
* Returns a map with all commits in tracked branches, keyed by hash.
*
* @return a map with all commits in tracked branches, keyed by hash.
* @throws VCSException
*/
public Map<String, CommitInfo> getCommitInfoTrackedMap() throws VCSException {
if (!initialized) {
initialize();
}
return commitInfoTrackedMap;
}
/**
* Returns a map with all commits in non-tracked branches, keyed by hash.
*
* @return a map with all commits in non-tracked branches, keyed by hash.
* @throws VCSException
*/
public Map<String, CommitInfo> getCommitInfoNonTrackedMap() throws VCSException {
if (!initialized) {
initialize();
}
return commitInfoNonTrackedMap;
}
/**
* Returns a set with all commits not found locally.
*
* @return a set with all commits not found locally.
* @throws VCSException
*/
public Set<CommitInfo> getCommitsNotFoundLocally() throws VCSException {
if (!initialized) {
initialize();
}
return notInLocalRepositoryListSet;
}
/**
* Returns a set with all commits not found in any of repositories that {@link #rep} pushes from.
*
* @return a set with all commits not found in any of the repositories that {@link #rep} pushes from.
* @throws VCSException
*/
public Set<CommitInfo> getCommitsNotInPushList() throws VCSException {
if (!initialized) {
initialize();
}
return notInPushListSet;
}
/**
* Returns a set with all commits not found in any of repositories that {@link #rep} pulls to.
*
* @return a set with all commits not found in any of the repositories that {@link #rep} pulls to.
* @throws VCSException
*/
public Set<CommitInfo> getCommitsNotInPullList() throws VCSException {
if (!initialized) {
initialize();
}
return notInPullListSet;
}
/**
* Loads external commits (not found locally) into the list of commits. After this, all known commits in the
* topology that includes {@link #rep} will be loaded into {@link #commitInfoMap}. The method also loads different
* sets, each one holding commits not found locally, commits not found in any of the push list repositories and
* commits not found in any of the pull list repositories.
*
* @param info The repository info to get related repositories from
*
* @throws DyeVCException
*/
public void loadExternalCommits(RepositoryInfo info) throws DyeVCException {
if (rep == null) {
throw new VCSException(
"Cannot include external commits without a monitored repository. Get an instance of "
+ "this class using one of the constructors that receive a MonitoredRepository as parameter.");
}
if (!initialized) {
initialize();
}
CommitDAO dao = new CommitDAO();
Set pushesToSet = new HashSet<String>(),
pullsFromSet = new HashSet<String>();
pushesToSet.addAll(info.getPushesTo());
pullsFromSet.addAll(info.getPullsFrom());
notInPullListSet = dao.getCommitsNotFoundInRepositories(pullsFromSet, info.getSystemName());
notInPushListSet = dao.getCommitsNotFoundInRepositories(pushesToSet, info.getSystemName());
notInLocalRepositoryListSet = dao.getCommitsNotFoundInRepository(rep.getId(), info.getSystemName());
includeExternalCommits();
}
/**
* Include the external commits in the {@link #commitInfoMap}, along with its relationships.
*/
private void includeExternalCommits() {
for (CommitInfo ci : notInLocalRepositoryListSet) {
commitInfoMap.put(ci.getHash(), ci);
}
for (CommitInfo ci : notInLocalRepositoryListSet) {
for (String hash : ci.getParents()) {
CommitRelationship cr = new CommitRelationship(ci, commitInfoMap.get(hash), false);
commitRelationshipList.add(cr);
}
}
}
/**
* Populates the commit history. This method traverses all commits in the Git repository. If commit does not yet
* exist in the commitInfoMap, than includes it.
*/
private void populateHistory() throws VCSException {
LoggerFactory.getLogger(GitCommitTools.class).trace("populateHistory -> Entry.");
try {
// separate tracked branches from local branches
Set<String> nonTrackedBranchesRefs = git.getLocalBranches();
Set<String> trackedBranchesRefs = new HashSet<String>();
Set<String> remoteBranchesRefs = git.getRemoteBranches();
List<TrackedBranch> trackedBranches = git.getTrackedBranches();
for (TrackedBranch tracked : trackedBranches) {
String ref = IConstants.REFS_HEADS + tracked.getName();
if (nonTrackedBranchesRefs.contains(ref)) {
nonTrackedBranchesRefs.remove(ref);
trackedBranchesRefs.add(ref);
}
}
trackedBranchesRefs.addAll(remoteBranchesRefs);
walk = new RevWalk(git.getRepository());
Iterator<RevCommit> it = git.getLogForHeads(trackedBranchesRefs);
while (it.hasNext()) {
RevCommit commit = it.next();
createCommitInfo(commit, true);
}
parseLocalCommits(nonTrackedBranchesRefs);
for (String commitId : commitInfoMap.keySet()) {
RevCommit commit = CommitUtils.getCommit(git.getRepository(), commitId);
createCommitRelations(commit);
}
LoggerFactory.getLogger(GitCommitTools.class).debug("populateHistory -> created history with {} items.",
commitInfoMap.size());
} catch (IOException ex) {
LoggerFactory.getLogger(GitCommitTools.class).error("Error in populateHistory.", ex);
throw new VCSException("Error getting repository history.", ex);
} finally {
if (walk != null) {
walk.dispose();
}
}
LoggerFactory.getLogger(GitCommitTools.class).trace("populateHistory -> Exit.");
}
/**
* Parse commits from the repository, starting with references specified in the branchHeads and taking all their
* parents, until all the repository commits are processed. The method stops when queue is empty.
*
* @param branchHeads List of heads for each branch to start traverse from. Taken from the refs/heads of the
* repository.
* @throws IOException
*/
private void parseLocalCommits(Set<String> branchHeads) throws IOException {
ArrayList<RevCommit> queue = new ArrayList<RevCommit>();
for (String ref : branchHeads) {
RevCommit commit = CommitUtils.getCommit(git.getRepository(), ref);
if (!commitInfoMap.containsKey(commit.getName())) {
queue.add(commit);
}
}
while (!queue.isEmpty()) {
RevCommit commit = walk.parseCommit(queue.remove(0));
createCommitInfo(commit, false);
for (RevCommit parent : commit.getParents()) {
if (!commitInfoMap.containsKey(parent.getName())) {
queue.add(parent);
}
}
}
}
/**
* Extracts the commit info from repository and creates an object containing the commit properties. After that,
* calls {@link #createCommitRelations(org.eclipse.jgit.revwalk.RevCommit, org.eclipse.jgit.revwalk.RevWalk)} to
* check relations between this commit and others.
*
* @param commit the repository commit object to extract properties from.
* @param tracked Indicates whether or not these set of references are heads of tracked branches
* @return a CommitInfo object
* @throws IOException
*/
private CommitInfo createCommitInfo(RevCommit commit, boolean tracked) throws IOException {
LoggerFactory.getLogger(GitCommitTools.class).trace("createCommitInfo -> Entry.");
CommitInfo ci = new CommitInfo(commit.getName(), git.getId());
ci.setCommitDate(new Date(commit.getCommitTime() * 1000L));
ci.setAuthor(commit.getAuthorIdent().getName());
ci.setCommitter(commit.getCommitterIdent().getName());
ci.setShortMessage(commit.getShortMessage());
ci.setRepositoryId(git.getId());
ci.setTracked(tracked);
if (includeTopologyData) {
ci.getFoundIn().add(rep.getId());
ci.setSystemName(rep.getSystemName());
}
commitInfoMap.put(ci.getHash(), ci);
if (tracked) {
commitInfoTrackedMap.put(ci.getHash(), ci);
} else {
commitInfoNonTrackedMap.put(ci.getHash(), ci);
}
LoggerFactory.getLogger(GitCommitTools.class).trace("createCommitInfo -> Exit.");
return ci;
}
/**
* Includes the existing relationships between the specified commit and others. Basically, the relationships consist
* of finding the parents of the specified commit.
*
* @param commit the commit to be checked for relationships
* @throws MissingObjectException
* @throws IncorrectObjectTypeException
* @throws IOException
*/
private void createCommitRelations(RevCommit commit)
throws MissingObjectException, IncorrectObjectTypeException, IOException {
LoggerFactory.getLogger(GitCommitTools.class).trace("createCommitRelations -> Entry.");
// gets the list of parents for the commit
RevCommit[] parents = commit.getParents();
for (int j = 0; j < parents.length; j++) {
// for each parent in the list, parses it, creating a RevCommit
RevCommit parent = walk.parseCommit(parents[j]);
CommitInfo child = commitInfoMap.get(commit.getName());
child.getParents().add(parent.getName());
CommitRelationship relation = new CommitRelationship(child, commitInfoMap.get(parent.getName()));
commitRelationshipList.add(relation);
}
LoggerFactory.getLogger(GitCommitTools.class).trace("createCommitRelations -> Exit.");
}
/**
* Overrides toString, showing all commits, along with its properties and relationships.
*
* @return the string representation of this commit history.
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder("CommitHistory:\n\t{Infos:\n");
for (Iterator<CommitInfo> it = commitInfoMap.values().iterator(); it.hasNext(); ) {
builder.append("\t\t").append(it.next()).append("\n");
}
builder.append("\tRelations:\n");
for (Iterator<CommitRelationship> it = commitRelationshipList.iterator(); it.hasNext(); ) {
builder.append("\t\t").append(it.next()).append("\n");
}
return builder.toString();
}
/**
* Gets the change set of a given revision string in the specified repository.
*
* @param commitId the commitId whose change set will be returned
* @param repositoryId the repository id to look into.
* @return the set of changes found in the given commit.
*/
public static Set<CommitChange> getCommitChangeSet(String commitId, String repositoryId) {
LoggerFactory.getLogger(GitCommitTools.class).trace("getCommitChangeSet -> Entry.");
Set<CommitChange> changes = new HashSet<CommitChange>();
RevWalk rw = null;
DiffFormatter df = null;
try {
Repository repo =
MonitoredRepositories.getMonitoredProjectById(repositoryId).getConnection().getRepository();
ObjectId objId = repo.resolve(commitId);
RevCommit commit = CommitUtils.getCommit(repo, objId);
rw = new RevWalk(repo);
RevCommit parent;
df = new DiffFormatter(DisabledOutputStream.INSTANCE);
df.setRepository(repo);
df.setDiffComparator(RawTextComparator.DEFAULT);
df.setDetectRenames(true);
List<DiffEntry> diffs;
if (commit.getParentCount() > 0) {
parent = rw.parseCommit(commit.getParent(0).getId());
diffs = df.scan(parent.getTree(), commit.getTree());
} else {
diffs = df.scan(new EmptyTreeIterator(),
new CanonicalTreeParser(null, rw.getObjectReader(), commit.getTree()));
}
for (DiffEntry diff : diffs) {
CommitChange cc = new CommitChange();
cc.setChangeType(diff.getChangeType().name());
cc.setOldPath(diff.getOldPath());
cc.setNewPath(diff.getNewPath());
changes.add(cc);
}
} catch (Exception ex) {
LoggerFactory.getLogger(GitCommitTools.class).error("Error parsing change set for commit " + commitId, ex);
} finally {
if (df != null) {
df.release();
}
if (rw != null) {
rw.dispose();
}
}
LoggerFactory.getLogger(GitCommitTools.class).trace("getCommitChangeSet -> Exit.");
return changes;
}
/**
* Get the common ancestral between the given revisions.
*
* @param revisions The revisions to start traversing the commit tree from.
* @return base commit or null if none
* @throws DyeVCException
*/
public CommitInfo getBase(final String... revisions) throws DyeVCException {
return new CommonAncestorFinder(commitInfoMap).getCommonAncestor(revisions);
}
/**
* Retrieves a map with every commit pointed by a branch head. The key is the commit hash and the value is the list
* of branch names that point to it.
*
* @return base commit or null if none
* @throws DyeVCException
*/
public Map<String, List<String>> getHeadsCommitsMap() throws DyeVCException {
if (commitsToBranchNamesMap == null) {
commitsToBranchNamesMap = new TreeMap<String, List<String>>();
Set<String> branches = git.getAllBranches();
for (String branch : branches) {
try {
String ref = git.getRepository().getRef(branch).getObjectId().name();
List<String> branchList = commitsToBranchNamesMap.get(ref);
if (branchList == null) {
branchList = new ArrayList<String>();
commitsToBranchNamesMap.put(ref, branchList);
}
branchList.add(branch);
} catch (IOException ex) {
LoggerFactory.getLogger(GitCommitTools.class).error("Error resolving a reference to " + branch, ex);
}
}
}
return commitsToBranchNamesMap;
}
/**
* Retrieves a map with every commit pointed by a tag. The key is the commit hash and the value is the list of tag
* names that point to it.
*
* @return base commit or null if none
* @throws DyeVCException
*/
public Map<String, List<String>> getTagsCommitsMap() throws DyeVCException {
if (commitsToTagNamesMap == null) {
commitsToTagNamesMap = new TreeMap<String, List<String>>();
Map<String, String> tags = git.getAllTags();
for (String tag : tags.keySet()) {
String commitHash = tags.get(tag);
List<String> tagList = commitsToTagNamesMap.get(commitHash);
if (tagList == null) {
tagList = new ArrayList<String>();
commitsToTagNamesMap.put(commitHash, tagList);
}
tagList.add(tag);
}
}
return commitsToTagNamesMap;
}
}