package de.hub.srcrepo;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.PumpStreamHandler;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.LockFailedException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.patch.HunkHeader;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import de.hub.jstattrack.TimeStatistic;
import de.hub.jstattrack.TimeStatistic.Timer;
import de.hub.jstattrack.services.BatchedPlot;
import de.hub.jstattrack.services.Summary;
import de.hub.srcrepo.repositorymodel.Diff;
import de.hub.srcrepo.repositorymodel.ParentRelation;
import de.hub.srcrepo.repositorymodel.RepositoryModel;
import de.hub.srcrepo.repositorymodel.RepositoryModelFactory;
import de.hub.srcrepo.repositorymodel.Rev;
public class GitSourceControlSystem implements ISourceControlSystem {
private final boolean isTryHard = false;
private Git git = null;
private DefaultExecutor executor = null;
private int importedRevCount = 0;
private final TimeStatistic gitCleanStat = new TimeStatistic(TimeUnit.MILLISECONDS).with(Summary.class).with(BatchedPlot.class).register(GitSourceControlSystem.class, "GitCleanET");
private final TimeStatistic gitResetStat = new TimeStatistic(TimeUnit.MILLISECONDS).with(Summary.class).with(BatchedPlot.class).register(GitSourceControlSystem.class, "GitResetET");
private final TimeStatistic gitCheckoutStat = new TimeStatistic(TimeUnit.MILLISECONDS).with(Summary.class).with(BatchedPlot.class).register(GitSourceControlSystem.class, "GitCheckoutET");
private final TimeStatistic gitCheckoutFullStat = new TimeStatistic(TimeUnit.MILLISECONDS).with(Summary.class).with(BatchedPlot.class).register(GitSourceControlSystem.class, "GitCheckoutFullET");
private DiffFormatter df = null;
@Override
public void createWorkingCopy(File target, String url, boolean onlyIfNecessary) throws SourceControlException {
CloneCommand cloneCommand = new CloneCommand();
cloneCommand.setCloneAllBranches(true);
cloneCommand.setRemote("origin");
cloneCommand.setURI(url);
if (!target.exists() || !onlyIfNecessary) {
if (target.exists()) {
try {
FileUtils.delete(target, FileUtils.RECURSIVE);
} catch (IOException e) {
throw new SourceControlException("Could not remove existing working copy: " + e.getMessage(), e);
}
}
cloneCommand.setDirectory(target);
try {
cloneCommand.call();
} catch (Exception e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
throw new SourceControlException("IOException during cloning.", e.getCause());
} else {
throw new SourceControlException(e);
}
}
}
setWorkingCopy(target);
}
@Override
public void setWorkingCopy(File target) throws SourceControlException {
try {
git = Git.open(target);
} catch (IOException e) {
throw new SourceControlException(e);
}
}
@Override
public File getWorkingCopy() {
return git.getRepository().getWorkTree();
}
@Override
public String getOrigin() {
String origin = null;
Config storedConfig = git.getRepository().getConfig();
Set<String> remotes = storedConfig.getSubsections("remote");
if (!remotes.isEmpty()) {
origin = storedConfig.getString("remote", remotes.iterator().next(), "url");
}
for (String remoteName : remotes) {
if (remoteName.toLowerCase().equals("origin")) {
origin = storedConfig.getString("remote", remotes.iterator().next(), "url");
}
}
if (origin == null) {
return "<unknown origin>";
} else {
return origin;
}
}
private void createParentRelation(RepositoryModelFactory factory, Rev childModel, Rev parentModel, List<DiffEntry> diffs) {
ParentRelation parentRelationModel = factory.createParentRelation();
childModel.getParentRelations().add(parentRelationModel);
parentRelationModel.setParent(parentModel);
for (DiffEntry diffEntry : diffs) {
int linesAdded = 0;
int linesRemoved = 0;
try {
FileHeader fileHeader = df.toFileHeader(diffEntry);
for(HunkHeader hunk: fileHeader.getHunks()) {
for(Edit edit: hunk.toEditList()) {
linesAdded += edit.getEndB() - edit.getBeginB();
linesRemoved += edit.getEndA() - edit.getBeginA();
}
}
} catch (Exception e) {
linesAdded = -1;
linesRemoved = -1;
SrcRepoActivator.INSTANCE.warning("Could not determine added/removed lines for " + diffEntry, e);
}
Diff diffModel = null;
String path = diffEntry.getNewPath();
diffModel = factory.createDiff();
diffModel.setNewPath(path);
diffModel.setOldPath(diffEntry.getOldPath());
diffModel.setType(diffEntry.getChangeType());
diffModel.setLinesAdded(linesAdded);
diffModel.setLinesRemoved(linesRemoved);
parentRelationModel.getDiffs().add(diffModel);
}
}
@Override
public void importRevisions(RepositoryModel model) throws SourceControlException {
try {
doImportRevisions(model);
} catch (Exception e) {
throw new SourceControlException("Could not import the repository.", e);
}
}
public void importRevisions(RepositoryModel model, String... names) throws Exception {
Repository jGitRepository = git.getRepository();
RevWalk walk = new RevWalk(jGitRepository);
ArrayList<RevCommit> commits = new ArrayList<RevCommit>();
for (String name: names) {
commits.add(walk.parseCommit(jGitRepository.resolve(name)));
}
walk.close();
walk.dispose();
doImportCommits(model, commits);
}
private void doImportRevisions(RepositoryModel model) throws Exception {
// create helper
RepositoryModelFactory factory = (RepositoryModelFactory)model.eClass().getEPackage().getEFactoryInstance();
List<RevCommit> commitsToImport = new ArrayList<RevCommit>();
Collection<String> commitNamesToImport = new HashSet<String>();
Repository jGitRepository = git.getRepository();
RevWalk walk = new RevWalk(jGitRepository);
// resolve all refs, these are our starting points
Map<AnyObjectId, Set<Ref>> allRefsByPeeledObjectId = jGitRepository.getAllRefsByPeeledObjectId();
Set<AnyObjectId> peeledRefsIds = allRefsByPeeledObjectId.keySet();
// walk from all starting refs down to collect all commits (TODO what about abandoned and removed branches)
for (AnyObjectId peeledRefId: peeledRefsIds) {
walk.reset();
RevObject peeledRef = walk.parseAny(peeledRefId);
if (peeledRef.getType() == Constants.OBJ_COMMIT) {
RevCommit startCommit = (RevCommit)peeledRef;
for (Ref ref: allRefsByPeeledObjectId.get(peeledRefId)) {
de.hub.srcrepo.repositorymodel.Ref refModel = createRefModel(factory, ref);
Rev startRevModel = getRevModel(model, factory, startCommit);
refModel.setReferencedCommit(startRevModel);
model.getAllRefs().add(refModel);
}
walk.markStart(startCommit);
for(RevCommit commit: walk) {
if (commitNamesToImport.add(commit.getName())) {
commitsToImport.add(commit);
}
}
}
}
walk.close();
walk.dispose();
SrcRepoActivator.INSTANCE.info("Found " + commitsToImport.size() + " commits based on " + peeledRefsIds.size() + " starting refs. Importing now...");
// import all found commits
doImportCommits(model, commitsToImport);
}
private void doImportCommits(RepositoryModel model, List<RevCommit> commitsToImport) throws Exception {
RepositoryModelFactory factory = (RepositoryModelFactory)model.eClass().getEPackage().getEFactoryInstance();
Repository jGitRepository = git.getRepository();
RevWalk walk = new RevWalk(jGitRepository);
ObjectReader objectReader = walk.getObjectReader();
df = new DiffFormatter(DisabledOutputStream.INSTANCE);
df.setContext(0);
df.setRepository(git.getRepository());
df.setDiffComparator(RawTextComparator.DEFAULT);
df.setDetectRenames(true);
for (RevCommit commit: commitsToImport) {
SrcRepoActivator.INSTANCE.debug("import revision " + commit.getName());
Rev revModel = getRevModel(model, factory, commit);
revModel.setTime(new Date(((long) commit.getCommitTime()) * 1000));
revModel.setMessage(commit.getFullMessage());
revModel.setAuthor(commit.getAuthorIdent().getName());
List<DiffEntry> diffs = null;
if (commit.getParentCount() > 0) {
for (RevCommit parent : commit.getParents()) {
diffs = df.scan(parent, commit);
Rev parentRevModel = getRevModel(model, factory, parent);
createParentRelation(factory, revModel, parentRevModel, diffs);
}
} else {
diffs = df.scan(new EmptyTreeIterator(), new CanonicalTreeParser(null, objectReader, commit.getTree()));
createParentRelation(factory, revModel, null, diffs);
if (!model.getRootRevs().isEmpty()) {
SrcRepoActivator.INSTANCE.warning("There are multiple root revisions, this should not happen: " + revModel.getName() + ", " + revModel.getTime() + ". Using the earlier one.");
}
SrcRepoActivator.INSTANCE.info("Root revision: " + revModel.getName() + ", " + revModel.getTime());
model.getRootRevs().add(revModel);
}
}
df.close();
walk.close();
walk.dispose();
}
private Rev getRevModel(RepositoryModel model, RepositoryModelFactory factory, RevCommit commit) throws Exception {
Rev revModel = RepositoryModelUtil.getRev(model, commit.getName());
if (revModel == null) {
String revName = commit.getName();
Rev newRevModel = RepositoryModelUtil.getRev(model, revName);
if (newRevModel == null) {
newRevModel = factory.createRev();
newRevModel.setName(revName);
model.getAllRevs().add(newRevModel);
// if (RepositoryModelUtil.getRev(model, revName) != newRevModel) {
// SrcRepoActivator.INSTANCE.error("Unexpected model state.");
// }
SrcRepoActivator.INSTANCE.debug("Created a model for revision " + newRevModel.getName() + " (" + importedRevCount++ +")");
newRevModel = RepositoryModelUtil.getRev(model, revName);
}
revModel = newRevModel;
}
return revModel;
}
private de.hub.srcrepo.repositorymodel.Ref createRefModel(RepositoryModelFactory factory, Ref ref) {
de.hub.srcrepo.repositorymodel.Ref refModel = factory.createRef();
refModel.setIsPeeled(ref.isPeeled());
refModel.setIsSymbolic(ref.isSymbolic());
refModel.setName(ref.getName());
return refModel;
}
private void clean() throws GitAPIException {
git.clean().setCleanDirectories(true).setIgnore(false).setDryRun(false).call();
if (isTryHard) {
org.eclipse.jgit.api.Status status = git.status().call();
if (!status.isClean()) {
// try again
git.clean().setCleanDirectories(true).setIgnore(false).setDryRun(false).call();
status = git.status().call();
}
if (!status.isClean() || status.getUntracked().size() > 0) {
SrcRepoActivator.INSTANCE.warning("Git clean did not fully clean even after trying again: " + status.isClean() + "/"
+ status.getUntracked().size() + ".");
}
}
}
private void removeLock(String file) {
File lockFile = new File(git.getRepository().getWorkTree().getPath() + "/" + file);
if (lockFile.exists()) {
lockFile.delete();
SrcRepoActivator.INSTANCE.debug("Had to remove git lock file " + file);
}
}
@Override
public void checkoutRevision(String name) throws SourceControlException {
Timer checkoutAllTimer = gitCheckoutFullStat.timer();
if (SrcRepoActivator.INSTANCE.useCGit) {
try {
if (executor == null) {
executor = new DefaultExecutor();
executor.setWorkingDirectory(git.getRepository().getWorkTree());
executor.setStreamHandler(new PumpStreamHandler(null, null, null));
}
Timer cleanTimer = gitCleanStat.timer();
executor.execute(CommandLine.parse("git clean -f"));
cleanTimer.track();
Timer resetTimer = gitResetStat.timer();
executor.execute(CommandLine.parse("git reset --hard"));
resetTimer.track();
Timer checkoutTimer = gitCheckoutStat.timer();
executor.execute(CommandLine.parse("git checkout -f " + name));
checkoutTimer.track();
} catch (Exception e) {
throw new SourceControlException(e);
} finally {
checkoutAllTimer.track();
}
} else {
try {
// remove a possible lock files from prior errors or crashes
removeLock(".git/index.lock");
removeLock(".git/HEAD.lock");
// clean the working tree from ignored or other untracked files
Timer cleanTimer = gitCleanStat.timer();
clean();
cleanTimer.track();
// reset possible changes
Timer resetTimer = gitResetStat.timer();
git.reset().setMode(ResetType.HARD).call();
resetTimer.track();
// checkout the new revision
Timer checkoutTimer = gitCheckoutStat.timer();
git.checkout().setForce(true).setName(name).call();
checkoutTimer.track();
} catch (JGitInternalException e) {
if (e.getCause() instanceof LockFailedException) {
throw new SourceControlException("Could not lock the repository.", e);
} if (e.getMessage().contains("conflict")) {
throw new SourceControlException("Conflicts during checkout.", e);
} else {
throw new SourceControlException(e);
}
} catch (GitAPIException e) {
throw new SourceControlException(e);
} finally {
checkoutAllTimer.track();
}
}
}
@Override
public void close() {
if (git != null) {
git.close();
}
}
}