blob: 6a9690b120a3ba41a11ef780fc04985a6f87bfe4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.git;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.SwingUtilities;
import org.netbeans.libs.git.GitException;
import org.netbeans.libs.git.GitRevisionInfo;
import org.netbeans.libs.git.GitUser;
import org.netbeans.modules.git.client.GitClient;
import org.netbeans.modules.git.client.GitProgressSupport;
import org.netbeans.modules.git.ui.checkout.RevertChangesAction;
import org.netbeans.modules.git.ui.history.SearchHistoryAction;
import org.netbeans.modules.git.utils.GitUtils;
import org.netbeans.modules.versioning.history.HistoryAction;
import org.netbeans.modules.versioning.spi.VCSContext;
import org.netbeans.modules.versioning.spi.VCSHistoryProvider;
import org.netbeans.modules.versioning.util.FileUtils;
import org.netbeans.modules.versioning.util.Utils;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction;
import org.openide.util.lookup.Lookups;
/**
*
* @author Tomas Stupka, Ondra Vrabec
*/
public class HistoryProvider implements VCSHistoryProvider {
private final List<VCSHistoryProvider.HistoryChangeListener> listeners = new LinkedList<VCSHistoryProvider.HistoryChangeListener>();
private static final Logger LOG = Logger.getLogger(HistoryProvider.class.getName());
private Action[] actions;
@Override
public void addHistoryChangeListener(VCSHistoryProvider.HistoryChangeListener l) {
synchronized(listeners) {
listeners.add(l);
}
}
@Override
public void removeHistoryChangeListener(VCSHistoryProvider.HistoryChangeListener l) {
synchronized(listeners) {
listeners.remove(l);
}
}
@Override
public synchronized HistoryEntry[] getHistory(File[] files, Date fromDate) {
assert !SwingUtilities.isEventDispatchThread() : "Accessing remote repository. Do not call in awt!";
Set<File> repositories = getRepositoryRoots(files);
if(repositories == null) {
return null;
}
List<HistoryEntry> ret = new LinkedList<HistoryEntry>();
Map<String, Set<File>> rev2FileMap = new HashMap<String, Set<File>>();
Map<String, GitRevisionInfo> rev2LMMap = new LinkedHashMap<String, GitRevisionInfo>();
File repositoryRoot = repositories.iterator().next();
for (File file : files) {
FileInformation info = Git.getInstance().getFileStatusCache().getStatus(file);
if (!info.containsStatus(FileInformation.STATUS_MANAGED)) {
continue;
}
GitRevisionInfo[] history;
try {
history = HistoryRegistry.getInstance().getLogs(repositoryRoot, files, fromDate, null, GitUtils.NULL_PROGRESS_MONITOR);
for (GitRevisionInfo h : history) {
String r = h.getRevision();
rev2LMMap.put(r, h);
Set<File> s = rev2FileMap.get(r);
if(s == null) {
s = new HashSet<File>();
rev2FileMap.put(r, s);
}
s.add(file);
}
} catch (GitException ex) {
LOG.log(Level.INFO, null, ex);
}
}
for (GitRevisionInfo h : rev2LMMap.values()) {
Set<File> s = rev2FileMap.get(h.getRevision());
File[] involvedFiles = s.toArray(new File[s.size()]);
HistoryEntry e = createHistoryEntry(h, involvedFiles, repositoryRoot);
ret.add(e);
}
return ret.toArray(new HistoryEntry[ret.size()]);
}
private HistoryEntry createHistoryEntry (GitRevisionInfo h, File[] involvedFiles, File repositoryRoot) {
GitUser user = h.getAuthor();
if (user == null) {
user = h.getCommitter();
}
String username = user.getName();
String author = user.toString();
String message = h.getFullMessage();
assert message != null;
HistoryEntry e = new HistoryEntry(
involvedFiles,
new Date(h.getCommitTime()),
message == null ? "" : message, //NOI18N
author,
username,
h.getRevision(),
h.getRevision().length() > 7 ? h.getRevision().substring(0, 7) : h.getRevision(),
getActions(),
new RevisionProviderImpl(h.getRevision()),
null,
new ParentProviderImpl(h, involvedFiles, repositoryRoot));
return e;
}
@Override
public Action createShowHistoryAction(File[] files) {
return new OpenHistoryAction(files);
}
public void fireHistoryChange (final File[] files) {
final HistoryChangeListener[] la;
synchronized(listeners) {
la = listeners.toArray(new HistoryChangeListener[listeners.size()]);
}
Git.getInstance().getRequestProcessor().post(new Runnable() {
@Override
public void run() {
for (HistoryChangeListener l : la) {
l.fireHistoryChanged(new HistoryEvent(HistoryProvider.this, files));
}
}
});
}
private class RevisionProviderImpl implements RevisionProvider {
private String revision;
public RevisionProviderImpl(String revision) {
this.revision = revision;
}
@Override
public void getRevisionFile (File originalFile, File revisionFile) {
assert !SwingUtilities.isEventDispatchThread() : "Accessing remote repository. Do not call in awt!";
try {
FileInformation info = Git.getInstance().getFileStatusCache().getStatus(originalFile);
if (info.containsStatus(FileInformation.Status.NEW_HEAD_INDEX) && info.getOldFile() != null) {
originalFile = info.getOldFile();
}
Set<File> repositories = getRepositoryRoots(originalFile);
if(repositories == null || repositories.isEmpty()) {
LOG.log(Level.WARNING, "Repository root not found for file {0}", originalFile);
return;
}
File repository = repositories.iterator().next();
File historyFile = HistoryRegistry.getInstance().getHistoryFile(repository, originalFile, revision, true);
if(historyFile != null) {
// ah! we already know the file was moved in the history,
// so lets look for contents by using its previous name
originalFile = historyFile;
}
File file = VersionsCache.getInstance().getFileRevision(originalFile, revision, GitUtils.NULL_PROGRESS_MONITOR);
if(file != null) {
FileUtils.copyFile(file, revisionFile); // XXX lets be faster - LH should cache that somehow ...
} else if(historyFile == null) {
// well then, lets try to find out if the file was move at some point in the history
LOG.log(Level.WARNING, "File {0} not found in revision {1}. Will make a guess ...", new Object[]{originalFile, revision});
historyFile = HistoryRegistry.getInstance().getHistoryFile(repository, originalFile, revision, false);
if(historyFile != null) {
file = VersionsCache.getInstance().getFileRevision(historyFile, revision, GitUtils.NULL_PROGRESS_MONITOR);
if(file != null) {
FileUtils.copyFile(file, revisionFile); // XXX lets be faster - LH should cache that somehow ...
}
}
}
} catch (IOException e) {
LOG.log(Level.WARNING, null, e);
}
}
}
private static class OpenHistoryAction extends AbstractAction {
private final File[] files;
public OpenHistoryAction(File[] files) {
this.files = files;
}
@Override
public void actionPerformed(ActionEvent e) {
openHistory(files);
}
private void openHistory(File[] files) {
if(files == null || files.length == 0) {
return;
}
Set<File> repositories = getRepositoryRoots(files);
if(repositories == null || repositories.isEmpty()) {
return;
}
List<Node> nodes = new ArrayList<Node>(files.length);
for (File f : files) {
nodes.add(new AbstractNode(Children.LEAF, Lookups.fixed(f)) {
@Override
public String getDisplayName() {
return getLookup().lookup(File.class).getName();
}
});
}
SearchHistoryAction.openSearch(repositories.iterator().next(), files, Utils.getContextDisplayName(VCSContext.forNodes(nodes.toArray(new Node[nodes.size()]))));
}
}
@NbBundle.Messages({
"# {0} - commit id", "HistoryProvider.action.RevertTo=Revert to {0}",
"HistoryProvider.action.RevertTo.progress=Reverting Files"
})
private synchronized Action[] getActions() {
if(actions == null) {
actions = new Action[] {
new HistoryAction(NbBundle.getMessage(SearchHistoryAction.class, "CTL_SummaryView_View")) {
@Override
protected void perform(HistoryEntry entry, Set<File> files) {
view(entry.getRevision(), false, files);
}
},
new HistoryAction(NbBundle.getMessage(SearchHistoryAction.class, "CTL_SummaryView_ShowAnnotations")) {
@Override
protected void perform(HistoryEntry entry, Set<File> files) {
view(entry.getRevision(), true, files);
}
},
new HistoryAction() {
@Override
protected void perform(final HistoryEntry entry, final Set<File> files) {
File root = Git.getInstance().getRepositoryRoot(files.iterator().next());
SystemAction.get(RevertChangesAction.class).revertFiles(root, files.toArray(new File[files.size()]),
getRevisionShort(), Bundle.HistoryProvider_action_RevertTo_progress());
}
@Override
protected boolean isMultipleHistory() {
return false;
}
@Override
public String getName() {
String rev = getRevisionShort();
if (rev == null) {
rev = ""; // NOI18N
}
return Bundle.HistoryProvider_action_RevertTo(rev);
}
@Override
public boolean isEnabled () {
return null != getRevisionShort();
}
},
};
}
return actions;
}
private void view(final String revision, final boolean showAnnotations, final Set<File> files) {
final File root = Git.getInstance().getRepositoryRoot(files.iterator().next());
new GitProgressSupport() {
@Override
protected void perform () {
for (File f : files) {
File original = HistoryRegistry.getInstance().getHistoryFile(root, f, revision, true);
if (original != null) {
f = original;
}
try {
GitUtils.openInRevision(f, -1, revision, showAnnotations, getProgressMonitor());
} catch (IOException ex) {
LOG.log(Level.INFO, null, ex);
}
}
}
}.start(Git.getInstance().getRequestProcessor(), root, NbBundle.getMessage(SearchHistoryAction.class, "MSG_SummaryView.openingFilesFromHistory")); //NOI18N
}
private static Set<File> getRepositoryRoots(File... files) {
Set<File> repositories = GitUtils.getRepositoryRoots(new HashSet<File>(Arrays.asList(files)));
if (repositories.size() != 1) {
LOG.log(Level.WARNING, "History requested for {0} repositories", repositories.size()); // NOI18N
return null;
}
return repositories;
}
private class ParentProviderImpl implements ParentProvider {
private GitRevisionInfo info;
private File[] files;
private File repository;
private Map<File, HistoryEntry> commonAncestors;
public ParentProviderImpl (GitRevisionInfo info, File[] files, File repository) {
this.info = info;
this.files = files;
this.repository = repository;
this.commonAncestors = new HashMap<File, HistoryEntry>(files.length);
}
@Override
public HistoryEntry getParentEntry (File file) {
HistoryEntry ancestorEntry = commonAncestors.get(file);
if (ancestorEntry == null && !commonAncestors.containsKey(file)) {
GitRevisionInfo parent = null;
GitClient client = null;
try {
client = Git.getInstance().getClient(repository);
if (info.getParents().length == 1) {
File historyFile = info.getModifiedFiles().containsKey(file)
? file
: HistoryRegistry.getInstance().getHistoryFile(repository, file, info.getRevision(), false);
if (historyFile != null) {
parent = client.getPreviousRevision(historyFile, info.getRevision(), GitUtils.NULL_PROGRESS_MONITOR);
}
} else if (info.getParents().length > 1) {
parent = client.getCommonAncestor(info.getParents(), GitUtils.NULL_PROGRESS_MONITOR);
}
} catch (GitException ex) {
LOG.log(Level.INFO, null, ex);
} finally {
if (client != null) {
client.release();
}
}
ancestorEntry = parent == null ? null : createHistoryEntry(parent, files, repository);
commonAncestors.put(file, ancestorEntry);
}
return ancestorEntry;
}
}
}