blob: c73a747e1350f86baa6a28e15c157ac6e2b46478 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2015 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Mathias Kinzler (SAP AG) - initial implementation
* Thomas Wolf <thomas.wolf@paranor.ch> - Bug 479108
* Simon Scholz <simon.scholz@vogella.com> - Bug 476505
*******************************************************************************/
package org.eclipse.egit.ui.internal.repository;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.RepositoryUtil;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.CommonUtils;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.components.CachedCheckboxTreeViewer;
import org.eclipse.egit.ui.internal.components.FilteredCheckboxTree;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IColorProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.PatternFilter;
import org.osgi.service.prefs.BackingStoreException;
/**
* Searches for Git directories under a path that can be selected by the user
*/
public class RepositorySearchDialog extends WizardPage {
private static final String PREF_DEEP_SEARCH = "RepositorySearchDialogDeepSearch"; //$NON-NLS-1$
private static final String PREF_SKIP_HIDDEN = "RepositorySearchDialogSkipHidden"; //$NON-NLS-1$
private static final String PREF_PATH = "RepositorySearchDialogSearchPath"; //$NON-NLS-1$
private final Set<String> fExistingDirectories = new HashSet<>();
private final boolean fillSearch;
private Set<String> fResult;
private FilteredCheckboxTree fTree;
private CachedCheckboxTreeViewer fTreeViewer;
private Text dir;
private Button lookForNestedButton;
private Button skipHiddenButton;
private Button searchButton;
private ToolItem checkAllItem;
private ToolItem uncheckAllItem;
private final ResourceManager fImageCache = new LocalResourceManager(
JFaceResources.getResources());
private final IEclipsePreferences prefs = InstanceScope.INSTANCE
.getNode(Activator.PLUGIN_ID);
private boolean allowBare;
private static final class ContentProvider implements ITreeContentProvider {
private final Object[] children = new Object[0];
@Override
@SuppressWarnings("unchecked")
public Object[] getElements(Object inputElement) {
return ((Set<String>) inputElement).toArray();
}
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
// nothing
}
@Override
public void dispose() {
// nothing
}
@Override
public Object[] getChildren(Object parentElement) {
// do not return null due to a bug in FilteredTree
return children;
}
@Override
public Object getParent(Object element) {
// nothing
return null;
}
@Override
public boolean hasChildren(Object element) {
// nothing
return false;
}
}
private final class RepositoryLabelProvider extends LabelProvider implements
IColorProvider {
@Override
public Image getImage(Object element) {
return fImageCache.createImage(UIIcons.REPOSITORY);
}
@Override
public String getText(Object element) {
return element.toString();
}
@Override
public Color getBackground(Object element) {
return null;
}
@Override
public Color getForeground(Object element) {
if (fExistingDirectories.contains(element))
return getShell().getDisplay().getSystemColor(SWT.COLOR_GRAY);
return null;
}
@Override
public void dispose() {
fImageCache.dispose();
}
}
/**
* @param existingDirs
*/
public RepositorySearchDialog(Collection<String> existingDirs) {
this(existingDirs, false, true);
}
/**
* @param existingDirs
* @param fillSearch
* true to fill search results when initially displayed
* @param allowBare
* if {@code true} allow bare repositories
*/
public RepositorySearchDialog(Collection<String> existingDirs,
boolean fillSearch, boolean allowBare) {
super("searchPage", UIText.RepositorySearchDialog_SearchTitle, //$NON-NLS-1$
UIIcons.WIZBAN_IMPORT_REPO);
this.fExistingDirectories.addAll(existingDirs);
this.fillSearch = fillSearch;
this.allowBare = allowBare;
}
/**
*
* @return the directories
*/
public Set<String> getDirectories() {
return fResult;
}
@Override
public void dispose() {
fResult = getCheckedItems();
super.dispose();
}
@Override
public void createControl(Composite parent) {
setMessage(UIText.RepositorySearchDialog_searchRepositoriesMessage);
Composite main = new Composite(parent, SWT.NONE);
main.setLayout(new GridLayout(1, false));
main.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
Group searchGroup = new Group(main, SWT.SHADOW_ETCHED_IN);
searchGroup.setText(UIText.RepositorySearchDialog_SearchCriteriaGroup);
searchGroup.setLayout(new GridLayout(4, false));
GridDataFactory.fillDefaults().grab(true, false)
.minSize(SWT.DEFAULT, SWT.DEFAULT).applyTo(searchGroup);
Label dirLabel = new Label(searchGroup, SWT.NONE);
dirLabel.setText(UIText.RepositorySearchDialog_directory);
dir = new Text(searchGroup, SWT.BORDER);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
.grab(true, false).hint(300, SWT.DEFAULT)
.minSize(100, SWT.DEFAULT).applyTo(dir);
dir.setToolTipText(UIText.RepositorySearchDialog_EnterDirectoryToolTip);
Button browse = new Button(searchGroup, SWT.PUSH);
browse.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false,
1, 1));
browse.setText(UIText.RepositorySearchDialog_browse);
browse.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
DirectoryDialog dd = new DirectoryDialog(getShell());
dd.setMessage(
UIText.RepositorySearchDialog_BrowseDialogMessage);
dd.setFilterPath(dir.getText());
String directory = dd.open();
if (directory != null) {
dir.setText(directory);
prefs.put(PREF_PATH, directory);
try {
prefs.flush();
} catch (BackingStoreException e1) {
// ignore here
}
doSearch();
}
}
});
searchButton = new Button(searchGroup, SWT.PUSH);
searchButton.setText(UIText.RepositorySearchDialog_Search);
searchButton
.setToolTipText(UIText.RepositorySearchDialog_SearchTooltip);
searchButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
doSearch();
}
});
lookForNestedButton = new Button(searchGroup, SWT.CHECK);
lookForNestedButton.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER,
false, false, 4, 1));
lookForNestedButton.setSelection(prefs.getBoolean(PREF_DEEP_SEARCH,
false));
lookForNestedButton
.setText(UIText.RepositorySearchDialog_DeepSearch_button);
lookForNestedButton
.setToolTipText(UIText.RepositorySearchDialog_SearchRecursiveToolTip);
lookForNestedButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
prefs.putBoolean(PREF_DEEP_SEARCH, lookForNestedButton
.getSelection());
try {
prefs.flush();
} catch (BackingStoreException e1) {
// ignore
}
setNeedsSearch();
}
});
skipHiddenButton = new Button(searchGroup, SWT.CHECK);
skipHiddenButton.setLayoutData(
new GridData(SWT.LEFT, SWT.CENTER, false, false, 4, 1));
skipHiddenButton
.setSelection(prefs.getBoolean(PREF_SKIP_HIDDEN, true));
skipHiddenButton.setText(UIText.RepositorySearchDialog_SkipHidden);
skipHiddenButton.setToolTipText(
UIText.RepositorySearchDialog_SkipHiddenTooltip);
skipHiddenButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
prefs.putBoolean(PREF_SKIP_HIDDEN, skipHiddenButton.getSelection());
try {
prefs.flush();
} catch (BackingStoreException e1) {
// ignore
}
setNeedsSearch();
}
});
Group searchResultGroup = new Group(main, SWT.SHADOW_ETCHED_IN);
searchResultGroup
.setText(UIText.RepositorySearchDialog_SearchResultGroup);
searchResultGroup.setLayout(new GridLayout(2, false));
GridDataFactory.fillDefaults().grab(true, true).minSize(SWT.DEFAULT, 0)
.applyTo(searchResultGroup);
PatternFilter filter = new PatternFilter() {
@Override
public boolean isElementVisible(Viewer viewer, Object element) {
if (getCheckedItems().contains(element)) {
return true;
}
return super.isElementVisible(viewer, element);
}
};
fTree = new FilteredCheckboxTree(searchResultGroup, null, SWT.NONE,
filter);
fTreeViewer = fTree.getCheckboxTreeViewer();
fTreeViewer.addCheckStateListener(new ICheckStateListener() {
@Override
public void checkStateChanged(CheckStateChangedEvent event) {
enableOk();
}
});
// Set a reasonable minimum height here; otherwise the dialog comes up
// with a tree that has only a few rows visible.
GridDataFactory.fillDefaults().grab(true, true).minSize(0, 300)
.applyTo(fTree);
ToolBar toolbar = new ToolBar(searchResultGroup, SWT.FLAT
| SWT.VERTICAL);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.BEGINNING).applyTo(
toolbar);
checkAllItem = new ToolItem(toolbar, SWT.PUSH);
checkAllItem
.setToolTipText(UIText.RepositorySearchDialog_CheckAllRepositories);
checkAllItem.setEnabled(false);
Image checkImage = UIIcons.CHECK_ALL.createImage();
UIUtils.hookDisposal(checkAllItem, checkImage);
checkAllItem.setImage(checkImage);
checkAllItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
fTreeViewer.setAllChecked(true);
enableOk();
}
});
uncheckAllItem = new ToolItem(toolbar, SWT.PUSH);
uncheckAllItem
.setToolTipText(UIText.RepositorySearchDialog_UncheckAllRepositories);
uncheckAllItem.setEnabled(false);
Image uncheckImage = UIIcons.UNCHECK_ALL.createImage();
UIUtils.hookDisposal(uncheckAllItem, uncheckImage);
uncheckAllItem.setImage(uncheckImage);
uncheckAllItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
fTreeViewer.setAllChecked(false);
enableOk();
}
});
// TODO this isn't the most optimal way of handling this... ideally we
// should have some type of delay
// if we could use databinding an observeDelayedValue would totally work
// here
dir.addModifyListener(e -> setNeedsSearch());
fTreeViewer.setContentProvider(new ContentProvider());
fTreeViewer.setLabelProvider(new RepositoryLabelProvider());
String initialPath = prefs.get(PREF_PATH,
RepositoryUtil.getDefaultRepositoryDir());
dir.setText(initialPath);
setControl(main);
enableOk();
if (fillSearch && searchButton.isEnabled()) {
PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
if (!getControl().isDisposed()) {
doSearch();
}
});
}
}
private String findGitDirsRecursive(Path root, final Set<Path> gitDirs,
IProgressMonitor monitor, final boolean lookForNested,
boolean skipHidden) {
long start = System.currentTimeMillis();
final int[] dirCount = new int[1];
final SubMonitor m = SubMonitor.convert(monitor);
try {
SimpleFileVisitor<Path> visitor = new SimpleFileVisitor<>() {
private long lastMonitorUpdate;
@Override
public FileVisitResult visitFileFailed(Path file,
IOException exc) throws IOException {
return FileVisitResult.SKIP_SUBTREE;
}
@Override
public FileVisitResult preVisitDirectory(Path d,
BasicFileAttributes attrs) throws IOException {
dirCount[0]++;
if (m.isCanceled()) {
return FileVisitResult.TERMINATE;
} else if (d == null) {
return FileVisitResult.CONTINUE;
} else if (isHidden(d) || isGitInternal(d)) {
return FileVisitResult.SKIP_SUBTREE;
}
updateMonitor(d);
Path resolved = resolve(d);
if (resolved == null) {
return FileVisitResult.CONTINUE;
}
if (!suppressed(resolved)) {
gitDirs.add(resolved.toAbsolutePath());
updateMonitor(resolved);
if (isDotGit(resolved)) { // non-bare
if (!lookForNested
|| (isSameFile(d, resolved)
&& !hasSubmodule(resolved))) {
return FileVisitResult.SKIP_SUBTREE;
}
} else { // bare
return FileVisitResult.SKIP_SUBTREE;
}
}
return FileVisitResult.CONTINUE;
}
private boolean isHidden(@NonNull Path d)
throws IOException {
return skipHidden && Files.isHidden(d)
&& !isDotGit(d);
}
private boolean isGitInternal(@NonNull Path d) {
Path fileName = d.getFileName();
if (fileName == null) {
return false;
}
Path p = d.getParent();
String n = fileName.toString();
return p != null && isDotGit(p)
&& !Constants.MODULES.equals(n);
}
private Path resolve(@NonNull Path d) {
File f = FileKey.resolve(d.toFile(), FS.DETECTED);
if (f == null) {
return null;
}
return f.toPath();
}
private boolean suppressed(@NonNull Path d) {
return !allowBare && !isDotGit(d);
}
private boolean isDotGit(@NonNull Path d) {
Path fileName = d.getFileName();
if (fileName == null) {
return false;
}
return Constants.DOT_GIT.equals(fileName.toString());
}
private boolean isSameFile(@NonNull Path f1, @NonNull Path f2) {
try {
return Files.isSameFile(f1, f2);
} catch (IOException e) {
return false;
}
}
private boolean hasSubmodule(@NonNull Path dotGit) {
Path gitmodules = dotGit.getParent()
.resolve(Constants.DOT_GIT_MODULES);
Path modules = dotGit.resolve(Constants.MODULES);
return Files.exists(gitmodules)
&& Files.exists(modules);
}
private void updateMonitor(@NonNull Path d) {
long now = System.currentTimeMillis();
if ((now - lastMonitorUpdate) > 100L) {
m.setWorkRemaining(100);
m.worked(1);
m.setTaskName(MessageFormat.format(
UIText.RepositorySearchDialog_RepositoriesFound_message,
Integer.valueOf(gitDirs.size()),
d.toAbsolutePath().toString()));
lastMonitorUpdate = now;
}
}
};
Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS),
Integer.MAX_VALUE, visitor);
} catch (IOException e) {
Activator.error(e.getMessage(), e);
}
long time = System.currentTimeMillis() - start;
return formatSummary(gitDirs, dirCount, time);
}
private String formatSummary(final Set<Path> gitDirs, final int[] dirCount,
long time) {
String fmtTime = ""; //$NON-NLS-1$
if (time < 1000) {
fmtTime = String.format("%dms", Long.valueOf(time)); //$NON-NLS-1$
} else if (time < 10000) {
fmtTime = String.format("%.1fs", Double.valueOf(time / 1000.0)); //$NON-NLS-1$
} else {
fmtTime = String.format("%ds", Long.valueOf(time / 1000)); //$NON-NLS-1$
}
return MessageFormat.format(UIText.RepositorySearchDialog_SearchResult,
Integer.valueOf(gitDirs.size()), Integer.valueOf(dirCount[0]),
fmtTime);
}
private HashSet<String> getCheckedItems() {
HashSet<String> ret = new HashSet<>();
for (Object item : fTreeViewer.getCheckedLeafElements())
ret.add((String) item);
return ret;
}
private void doSearch() {
setMessage(UIText.RepositorySearchDialog_searchRepositoriesMessage);
setErrorMessage(null);
// perform the search...
final Set<Path> directories = new TreeSet<>(
CommonUtils.PATH_STRING_COMPARATOR);
final Path file = Paths.get(dir.getText());
final boolean lookForNested = lookForNestedButton.getSelection();
final boolean skipHidden = skipHiddenButton.getSelection();
if (!Files.isDirectory(file)) {
return;
}
prefs.put(PREF_PATH, file.toAbsolutePath().toString());
try {
prefs.flush();
} catch (BackingStoreException e1) {
// ignore here
}
final TreeSet<String> validDirs = new TreeSet<>(getCheckedItems());
final String[] summary = new String[1];
IRunnableWithProgress action = new IRunnableWithProgress() {
@Override
public void run(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
monitor.beginTask(
UIText.RepositorySearchDialog_ScanningForRepositories_message,
IProgressMonitor.UNKNOWN);
try {
summary[0] = findGitDirsRecursive(file, directories, monitor,
lookForNested, skipHidden);
} catch (Exception ex) {
throw new InvocationTargetException(ex);
}
if (monitor.isCanceled()) {
throw new InterruptedException();
}
}
};
try {
getContainer().run(true, true, action);
} catch (InvocationTargetException e1) {
org.eclipse.egit.ui.Activator.handleError(
UIText.RepositorySearchDialog_errorOccurred, e1.getCause(),
true);
} catch (InterruptedException e1) {
// ignore
}
int foundOld = 0;
for (Path foundDir : directories) {
String absolutePath = foundDir.toAbsolutePath().toString();
if (!fExistingDirectories.contains(absolutePath)
&& !fExistingDirectories.contains(FileUtils
.canonicalize(foundDir.toFile())
.getAbsolutePath())) {
validDirs.add(absolutePath);
} else {
foundOld++;
}
}
if (foundOld > 0) {
String message = summary[0] + '\n'
+ MessageFormat.format(
UIText.RepositorySearchDialog_SomeDirectoriesHiddenMessage,
Integer.valueOf(foundOld));
setMessage(message, IMessageProvider.INFORMATION);
} else if (directories.isEmpty()) {
setMessage(summary[0], IMessageProvider.INFORMATION);
} else {
setMessage(summary[0]);
}
checkAllItem.setEnabled(!validDirs.isEmpty());
uncheckAllItem.setEnabled(!validDirs.isEmpty());
fTree.clearFilter();
// Remove the minimum height that was set initially so that we get a
// scrollbar when the dialog is resized.
GridDataFactory.fillDefaults().grab(true, true).applyTo(fTree);
fTreeViewer.setInput(validDirs);
if (!validDirs.isEmpty()) {
fTree.getFilterControl().setFocus();
}
enableOk();
}
private void setNeedsSearch() {
fTreeViewer.setInput(null);
try {
Path file = Paths.get(dir.getText());
if (!Files.isDirectory(file)) {
setErrorMessage(MessageFormat.format(
UIText.RepositorySearchDialog_DirectoryNotFoundMessage,
dir.getText()));
searchButton.setEnabled(false);
} else {
searchButton.setEnabled(true);
setErrorMessage(null);
setMessage(
UIText.RepositorySearchDialog_NoSearchAvailableMessage,
IMessageProvider.INFORMATION);
}
} catch (InvalidPathException e) {
setErrorMessage(MessageFormat.format(
UIText.RepositorySearchDialog_InvalidDirectoryMessage,
e.getLocalizedMessage()));
searchButton.setEnabled(false);
}
enableOk();
}
private void enableOk() {
boolean enable = fTreeViewer.getCheckedElements().length > 0;
setPageComplete(enable);
}
}