/*******************************************************************************
* Copyright (C) 2009, Alex Blewitt <alex.blewitt@gmail.com>
* Copyright (C) 2010, Jens Baumgart <jens.baumgart@sap.com>
* Copyright (C) 2012, 2013 Robin Stocker <robin@nibor.org>
* Copyright (C) 2015, Stephan Hackstedt <stephan.hackstedt@googlemail.com>
* Copyright (C) 2016, Thomas Wolf <thomas.wolf@paranor.ch>
*
* 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.core.op;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.IteratorService;
import org.eclipse.egit.core.internal.CoreText;
import org.eclipse.egit.core.internal.job.RuleUtil;
import org.eclipse.egit.core.internal.util.ResourceUtil;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.osgi.util.NLS;
/**
* IgnoreOperation adds resources to a .gitignore file
*
*/
public class IgnoreOperation implements IEGitOperation {
private final Collection<IPath> paths;
private boolean gitignoreOutsideWSChanged;
private ISchedulingRule schedulingRule;
/**
* construct an IgnoreOperation
*
* @param paths
* @since 2.2
*/
public IgnoreOperation(Collection<IPath> paths) {
this.paths = paths;
gitignoreOutsideWSChanged = false;
schedulingRule = calcSchedulingRule();
}
/**
* @param resources
* @deprecated use {@link #IgnoreOperation(Collection)}
*/
@Deprecated
public IgnoreOperation(IResource[] resources) {
paths = new ArrayList<IPath>(resources.length);
for (IResource resource : resources) {
IPath location = resource.getLocation();
if (location != null)
paths.add(location);
}
gitignoreOutsideWSChanged = false;
schedulingRule = calcSchedulingRule();
}
@Override
public void execute(IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor,
CoreText.IgnoreOperation_taskName, 3);
try {
Map<IPath, Collection<String>> perFolder = getFolderMap(
progress.newChild(1));
if (perFolder == null) {
return;
}
perFolder = pruneFolderMap(perFolder, progress.newChild(1));
if (perFolder == null) {
return;
}
updateGitIgnores(perFolder, progress.newChild(1));
} catch (CoreException e) {
throw e;
} catch (Exception e) {
throw new CoreException(Activator.error(
CoreText.IgnoreOperation_error, e));
}
}
/**
* @return true if a gitignore file outside the workspace was changed. In
* this case the caller may need to perform manual UI refreshes
* because there was no ResourceChanged event.
*/
public boolean isGitignoreOutsideWSChanged() {
return gitignoreOutsideWSChanged;
}
@Override
public ISchedulingRule getSchedulingRule() {
return schedulingRule;
}
private Map<IPath, Collection<String>> getFolderMap(
IProgressMonitor monitor) {
SubMonitor progress = SubMonitor.convert(monitor, paths.size());
Map<IPath, Collection<String>> result = new HashMap<>();
for (IPath path : paths) {
if (progress.isCanceled()) {
return null;
}
IPath parent = path.removeLastSegments(1);
Collection<String> values = result.get(parent);
if (values == null) {
values = new LinkedHashSet<>();
result.put(parent, values);
}
values.add(path.lastSegment());
progress.worked(1);
}
return result;
}
private Map<IPath, Collection<String>> pruneFolderMap(
Map<IPath, Collection<String>> perFolder, IProgressMonitor monitor)
throws IOException {
SubMonitor progress = SubMonitor.convert(monitor, perFolder.size());
for (Map.Entry<IPath, Collection<String>> entry : perFolder
.entrySet()) {
pruneFolder(entry.getKey(), entry.getValue(), progress.newChild(1));
if (progress.isCanceled()) {
return null;
}
}
return perFolder;
}
private void pruneFolder(IPath folder, Collection<String> files,
IProgressMonitor monitor)
throws IOException {
if (files.isEmpty()) {
return;
}
Repository repository = Activator.getDefault().getRepositoryCache()
.getRepository(folder);
if (repository == null || repository.isBare()) {
files.clear();
return;
}
WorkingTreeIterator treeIterator = IteratorService
.createInitialIterator(repository);
if (treeIterator == null) {
files.clear();
return;
}
IPath repoRelativePath = folder.makeRelativeTo(
new Path(repository.getWorkTree().getAbsolutePath()));
if (repoRelativePath.equals(folder)) {
files.clear();
return;
}
Collection<String> repoRelative = new HashSet<>(files.size());
for (String file : files) {
repoRelative.add(repoRelativePath.append(file).toPortableString());
}
// Remove all entries,then re-add only those found during the tree walk
// that are not ignored already
files.clear();
try (TreeWalk walk = new TreeWalk(repository)) {
walk.addTree(treeIterator);
walk.setFilter(PathFilterGroup.createFromStrings(repoRelative));
while (walk.next()) {
if (monitor.isCanceled()) {
return;
}
WorkingTreeIterator workingTreeIterator = walk.getTree(0,
WorkingTreeIterator.class);
if (repoRelative.contains(walk.getPathString())) {
if (!workingTreeIterator.isEntryIgnored()) {
files.add(walk.getNameString());
}
} else if (workingTreeIterator.getEntryFileMode()
.equals(FileMode.TREE)) {
walk.enterSubtree();
}
}
}
}
private void updateGitIgnores(Map<IPath, Collection<String>> perFolder,
IProgressMonitor monitor) throws CoreException, IOException {
SubMonitor progress = SubMonitor.convert(monitor, perFolder.size() * 2);
for (Map.Entry<IPath, Collection<String>> entry : perFolder
.entrySet()) {
if (progress.isCanceled()) {
return;
}
IContainer container = ResourceUtil
.getContainerForLocation(entry.getKey(), false);
if (container instanceof IWorkspaceRoot) {
container = null;
}
Collection<String> files = entry.getValue();
if (files.isEmpty()) {
progress.worked(1);
continue;
}
StringBuilder builder = new StringBuilder();
for (String file : files) {
builder.append('/').append(file);
boolean isDirectory = false;
IResource resource = container != null
? container.findMember(file) : null;
if (resource != null) {
isDirectory = resource.getType() != IResource.FILE;
} else {
isDirectory = entry.getKey().append(file).toFile()
.isDirectory();
}
if (isDirectory) {
builder.append('/');
}
builder.append('\n');
}
progress.worked(1);
if (progress.isCanceled()) {
return;
}
addToGitIgnore(container, entry.getKey(), builder.toString(),
progress.newChild(1));
}
}
private void addToGitIgnore(IContainer container, IPath parent,
String entry, IProgressMonitor monitor)
throws CoreException, IOException {
SubMonitor progress = SubMonitor.convert(monitor, 1);
if (container == null) {
// .gitignore outside of workspace
Repository repository = Activator.getDefault().getRepositoryCache()
.getRepository(parent);
if (repository == null || repository.isBare()) {
String message = NLS.bind(
CoreText.IgnoreOperation_parentOutsideRepo,
parent.toOSString(), null);
IStatus status = Activator.error(message, null);
throw new CoreException(status);
}
IPath gitIgnorePath = parent.append(Constants.GITIGNORE_FILENAME);
IPath repoPath = new Path(repository.getWorkTree()
.getAbsolutePath());
if (!repoPath.isPrefixOf(gitIgnorePath)) {
String message = NLS.bind(
CoreText.IgnoreOperation_parentOutsideRepo,
parent.toOSString(), repoPath.toOSString());
IStatus status = Activator.error(message, null);
throw new CoreException(status);
}
File gitIgnore = new File(gitIgnorePath.toOSString());
updateGitIgnore(gitIgnore, entry);
// no resource change event when updating .gitignore outside
// workspace => trigger manual decorator refresh
gitignoreOutsideWSChanged = true;
} else {
// .gitignore is in workspace
IFile gitignore = container.getFile(new Path(
Constants.GITIGNORE_FILENAME));
String toAdd = getEntry(gitignore.getLocation().toFile(), entry);
ByteArrayInputStream entryBytes = asStream(toAdd);
if (gitignore.exists()) {
gitignore.appendContents(entryBytes, true, true,
progress.newChild(1));
} else {
gitignore.create(entryBytes, true, progress.newChild(1));
}
}
}
private boolean prependNewline(File file) throws IOException {
boolean prepend = false;
long length = file.length();
if (length > 0) {
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { //$NON-NLS-1$
// Read the last byte and see if it is a newline
ByteBuffer buffer = ByteBuffer.allocate(1);
FileChannel channel = raf.getChannel();
channel.position(length - 1);
if (channel.read(buffer) > 0) {
buffer.rewind();
prepend = buffer.get() != '\n';
}
}
}
return prepend;
}
private String getEntry(File file, String entry) throws IOException {
return prependNewline(file) ? '\n' + entry : entry;
}
private void updateGitIgnore(File gitIgnore, String entry)
throws CoreException {
try {
String ignoreLine = entry;
if (!gitIgnore.exists()) {
if (!gitIgnore.createNewFile()) {
String error = NLS.bind(
CoreText.IgnoreOperation_creatingFailed,
gitIgnore.getAbsolutePath());
throw new CoreException(Activator.error(error, null));
}
} else {
ignoreLine = getEntry(gitIgnore, ignoreLine);
}
FileOutputStream os = new FileOutputStream(gitIgnore, true);
try {
os.write(ignoreLine.getBytes(Constants.CHARACTER_ENCODING));
} finally {
os.close();
}
} catch (IOException e) {
String error = NLS.bind(CoreText.IgnoreOperation_updatingFailed,
gitIgnore.getAbsolutePath());
throw new CoreException(Activator.error(error, e));
}
}
private ByteArrayInputStream asStream(String entry)
throws UnsupportedEncodingException {
return new ByteArrayInputStream(
entry.getBytes(Constants.CHARACTER_ENCODING));
}
private ISchedulingRule calcSchedulingRule() {
return RuleUtil.getRuleForContainers(paths);
}
}