/*******************************************************************************
* Copyright (C) 2008, 2009 Robin Rosenberg <robin.rosenberg@dewire.com>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.egit.ui.internal.decorators;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.WeakHashMap;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.egit.core.GitProvider;
import org.eclipse.egit.core.internal.CompareCoreUtils;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
import org.eclipse.jface.text.Document;
import org.eclipse.jgit.diff.DiffConfig;
import org.eclipse.jgit.diff.DiffConfig.RenameDetectionType;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.RenameDetector;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.events.ListenerHandle;
import org.eclipse.jgit.events.RefsChangedEvent;
import org.eclipse.jgit.events.RefsChangedListener;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.RepositoryProvider;
class GitDocument extends Document implements RefsChangedListener {
private final IResource resource;
private ObjectId lastCommit;
private ObjectId lastTree;
private ObjectId lastBlob;
private ListenerHandle myRefsChangedHandle;
// Job that reloads the document when something has changed
private Job reloadJob;
private boolean disposed;
static Map<GitDocument, Repository> doc2repo = new WeakHashMap<GitDocument, Repository>();
static GitDocument create(final IResource resource) throws IOException {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) create: " + resource); //$NON-NLS-1$
GitDocument ret = null;
if (RepositoryProvider.getProvider(resource.getProject()) instanceof GitProvider) {
ret = new GitDocument(resource);
ret.populate();
final Repository repository = ret.getRepository();
if (repository != null)
ret.myRefsChangedHandle = repository.getListenerList()
.addRefsChangedListener(ret);
}
return ret;
}
private GitDocument(IResource resource) {
this.resource = resource;
synchronized (doc2repo) {
doc2repo.put(this, getRepository());
}
}
private void setResolved(final AnyObjectId commit, final AnyObjectId tree,
final AnyObjectId blob, final String value) {
lastCommit = commit != null ? commit.copy() : null;
lastTree = tree != null ? tree.copy() : null;
lastBlob = blob != null ? blob.copy() : null;
set(value);
if (blob != null)
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation
.getTrace()
.trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) resolved " + resource + " to " + lastBlob + " in " + lastCommit + "/" + lastTree); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
else if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) unresolved " + resource); //$NON-NLS-1$
}
void populate() throws IOException {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().traceEntry(
GitTraceLocation.QUICKDIFF.getLocation(), resource);
// Do not populate if already disposed
if (disposed)
return;
TreeWalk tw = null;
RevWalk rw = null;
try {
RepositoryMapping mapping = RepositoryMapping.getMapping(resource);
if (mapping == null) {
setResolved(null, null, null, ""); //$NON-NLS-1$
return;
}
final String gitPath = mapping.getRepoRelativePath(resource);
if (gitPath == null) {
setResolved(null, null, null, ""); //$NON-NLS-1$
return;
}
final Repository repository = mapping.getRepository();
String baseline = GitQuickDiffProvider.baseline.get(repository);
if (baseline == null)
baseline = Constants.HEAD;
ObjectId commitId = repository.resolve(baseline);
if (commitId != null) {
if (commitId.equals(lastCommit)) {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) already resolved"); //$NON-NLS-1$
return;
}
} else {
if (repository.getRef(Constants.HEAD) == null) {
// Complain only if not an unborn branch
String msg = NLS.bind(UIText.GitDocument_errorResolveQuickdiff,
new Object[] { baseline, resource, repository });
Activator.logError(msg, new Throwable());
}
setResolved(null, null, null, ""); //$NON-NLS-1$
return;
}
rw = new RevWalk(repository);
RevCommit baselineCommit;
ObjectReader reader = null;
String oldPath = gitPath;
try {
reader = repository.newObjectReader();
baselineCommit = rw.parseCommit(commitId);
DiffConfig diffConfig = repository.getConfig().get(
DiffConfig.KEY);
if (diffConfig.getRenameDetectionType() != RenameDetectionType.FALSE) {
TreeWalk walk = new TreeWalk(repository);
CanonicalTreeParser baseLineIterator = new CanonicalTreeParser();
baseLineIterator.reset(reader, baselineCommit.getTree());
walk.addTree(baseLineIterator);
walk.addTree(new DirCacheIterator(repository.readDirCache()));
List<DiffEntry> diffs = DiffEntry.scan(walk, true);
RenameDetector renameDetector = new RenameDetector(
repository);
renameDetector.addAll(diffs);
List<DiffEntry> renames = renameDetector.compute();
for (DiffEntry e : renames) {
if (e.getNewPath().equals(gitPath)) {
oldPath = e.getOldPath();
break;
}
}
}
} catch (IOException err) {
String msg = NLS
.bind(UIText.GitDocument_errorLoadCommit, new Object[] {
commitId, baseline, resource, repository });
Activator.logError(msg, err);
setResolved(null, null, null, ""); //$NON-NLS-1$
return;
} finally {
if (reader != null)
reader.release();
rw.dispose();
}
RevTree treeId = baselineCommit.getTree();
if (treeId.equals(lastTree)) {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) already resolved"); //$NON-NLS-1$
return;
}
tw = TreeWalk.forPath(repository, oldPath, treeId);
if (tw == null) {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation
.getTrace()
.trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) resource " + resource + " not found in " + treeId + " in " + repository + ", baseline=" + baseline); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
setResolved(null, null, null, ""); //$NON-NLS-1$
return;
}
ObjectId id = tw.getObjectId(0);
if (id.equals(ObjectId.zeroId())) {
setResolved(null, null, null, ""); //$NON-NLS-1$
String msg = NLS
.bind(UIText.GitDocument_errorLoadTree, new Object[] {
treeId.getName(), baseline, resource, repository });
Activator.logError(msg, new Throwable());
setResolved(null, null, null, ""); //$NON-NLS-1$
return;
}
if (!id.equals(lastBlob)) {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) compareTo: " + baseline); //$NON-NLS-1$
ObjectLoader loader = repository.open(id, Constants.OBJ_BLOB);
byte[] bytes = loader.getBytes();
String charset;
charset = CompareCoreUtils.getResourceEncoding(resource);
// Finally we could consider validating the content with respect
// to the content. We don't do that here.
String s = new String(bytes, charset);
setResolved(commitId, treeId, id, s);
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation
.getTrace()
.trace(GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) has reference doc, size=" + s.length() + " bytes"); //$NON-NLS-1$ //$NON-NLS-2$
} else {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) already resolved"); //$NON-NLS-1$
}
} finally {
if (tw != null)
tw.release();
if (rw != null)
rw.release();
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().traceExit(
GitTraceLocation.QUICKDIFF.getLocation());
}
}
void dispose() {
if (GitTraceLocation.QUICKDIFF.isActive())
GitTraceLocation.getTrace().trace(
GitTraceLocation.QUICKDIFF.getLocation(),
"(GitDocument) dispose: " + resource); //$NON-NLS-1$
synchronized (doc2repo) {
doc2repo.remove(this);
}
if (myRefsChangedHandle != null) {
myRefsChangedHandle.remove();
myRefsChangedHandle = null;
}
cancelReloadJob();
disposed = true;
}
public void onRefsChanged(final RefsChangedEvent event) {
cancelReloadJob();
reloadJob = new Job(UIText.GitDocument_ReloadJobName) {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
populate();
return Status.OK_STATUS;
} catch (IOException e) {
return Activator.createErrorStatus(
UIText.GitDocument_ReloadJobError, e);
}
}
};
reloadJob.schedule();
}
private void cancelReloadJob() {
if (reloadJob != null && reloadJob.getState() != Job.NONE)
reloadJob.cancel();
}
private Repository getRepository() {
RepositoryMapping mapping = RepositoryMapping.getMapping(resource);
return (mapping != null) ? mapping.getRepository() : null;
}
/**
* A change occurred to a repository. Update any GitDocument instances
* referring to such repositories.
*
* @param repository
* Repository which changed
* @throws IOException
*/
static void refreshRelevant(final Repository repository) throws IOException {
final Entry[] docs;
synchronized (doc2repo) {
docs = doc2repo.entrySet().toArray(new Entry[doc2repo.size()]);
}
for (Entry doc : docs)
if (doc.getValue() == repository)
((GitDocument) doc.getKey()).populate();
}
}