/*
* A Gradle plugin for the creation of Minecraft mods and MinecraftForge plugins.
* Copyright (C) 2013 Minecraft Forge
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
package net.minecraftforge.gradle.patcher;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import net.minecraftforge.gradle.util.SequencedInputSupplier;
import net.minecraftforge.srg2source.util.io.FolderSupplier;
import net.minecraftforge.srg2source.util.io.InputSupplier;
import net.minecraftforge.srg2source.util.io.ZipInputSupplier;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import com.cloudbees.diff.Diff;
import com.cloudbees.diff.Hunk;
import com.cloudbees.diff.PatchException;
import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
class TaskGenPatches extends DefaultTask
{
//@formatter:off
@OutputDirectory private Object patchDir;
private final List<Object> originals = new LinkedList<Object>();
private final List<Object> changed = new LinkedList<Object>();
@Input private String originalPrefix = "";
@Input private String changedPrefix = "";
//@formatter:on
//@formatter:off
public TaskGenPatches() { super(); }
//@formatter:on
private Set<File> created = new HashSet<File>();
@TaskAction
public void doTask() throws IOException, PatchException
{
created.clear();
getPatchDir().mkdirs();
// fix and create patches.
processFiles(getSupplier(getOriginalSource()), getSupplier(getChangedSource()));
removeOld(getPatchDir());
}
private static InputSupplier getSupplier(List<File> files) throws IOException
{
SequencedInputSupplier supplier = new SequencedInputSupplier(files.size() + 1);
for (File f : files)
{
if (f.isDirectory())
supplier.add(new FolderSupplier(f));
else
{
ZipInputSupplier supp = new ZipInputSupplier();
supp.readZip(f);
supplier.add(supp);
}
}
return supplier;
}
private void removeOld(File dir) throws IOException
{
final ArrayList<File> directories = new ArrayList<File>();
FileTree tree = getProject().fileTree(dir);
tree.visit(new FileVisitor()
{
@Override
public void visitDir(FileVisitDetails dir)
{
directories.add(dir.getFile());
}
@Override
public void visitFile(FileVisitDetails f)
{
File file;
try
{
file = f.getFile().getCanonicalFile();
if (!created.contains(file))
{
getLogger().debug("Removed patch: " + f.getRelativePath());
file.delete();
}
}
catch (IOException e)
{
// impossibru
}
}
});
// We want things sorted in reverse order. Do that sub folders come before parents
Collections.sort(directories, new Comparator<File>()
{
@Override
public int compare(File o1, File o2)
{
int r = o1.compareTo(o2);
if (r < 0) return 1;
if (r > 0) return -1;
return 0;
}
});
for (File f : directories)
{
if (f.listFiles().length == 0)
{
getLogger().debug("Removing empty dir: " + f);
f.delete();
}
}
}
public void processFiles(InputSupplier original, InputSupplier changed) throws IOException
{
List<String> paths = original.gatherAll("");
for (String path : paths)
{
path = path.replace('\\', '/');
InputStream o = original.getInput(path);
InputStream c = changed.getInput(path);
try
{
processFile(path, o, c);
}
finally
{
if (o != null) o.close();
if (c != null) c.close();
}
}
}
public void processFile(String relative, InputStream original, InputStream changed) throws IOException
{
getLogger().debug("Diffing: " + relative);
File patchFile = new File(getPatchDir(), relative + ".patch").getCanonicalFile();
if (changed == null)
{
getLogger().debug(" Changed File does not exist");
return;
}
// We have to cache the bytes because diff reads the stream twice.. why.. who knows.
byte[] oData = ByteStreams.toByteArray(original);
byte[] cData = ByteStreams.toByteArray(changed);
Diff diff = Diff.diff(new InputStreamReader(new ByteArrayInputStream(oData), Charsets.UTF_8), new InputStreamReader(new ByteArrayInputStream(cData), Charsets.UTF_8), false);
if (!relative.startsWith("/"))
relative = "/" + relative;
if (!diff.isEmpty())
{
String unidiff = diff.toUnifiedDiff(originalPrefix + relative, changedPrefix + relative,
new InputStreamReader(new ByteArrayInputStream(oData), Charsets.UTF_8),
new InputStreamReader(new ByteArrayInputStream(cData), Charsets.UTF_8), 3);
unidiff = unidiff.replace("\r\n", "\n"); //Normalize lines
unidiff = unidiff.replace("\n" + Hunk.ENDING_NEWLINE + "\n", "\n"); //We give 0 shits about this.
String olddiff = "";
if (patchFile.exists())
{
olddiff = Files.toString(patchFile, Charsets.UTF_8);
}
if (!olddiff.equals(unidiff))
{
getLogger().debug("Writing patch: " + patchFile);
patchFile.getParentFile().mkdirs();
Files.touch(patchFile);
Files.write(unidiff, patchFile, Charsets.UTF_8);
}
else
{
getLogger().debug("Patch did not change");
}
created.add(patchFile);
}
}
@InputFiles
public FileCollection getOriginalSources()
{
return getProject().files(originals);
}
public List<File> getOriginalSource()
{
List<File> files = new LinkedList<File>();
for (Object f : originals)
files.add(getProject().file(f));
return files;
}
public void addOriginalSource(Object in)
{
this.originals.add(in);
}
@InputFiles
public FileCollection getChangedSources()
{
return getProject().files(changed);
}
public List<File> getChangedSource()
{
List<File> files = new LinkedList<File>();
for (Object f : changed)
files.add(getProject().file(f));
return files;
}
public void addChangedSource(Object in)
{
this.changed.add(in);
}
public File getPatchDir()
{
return getProject().file(patchDir);
}
public void setPatchDir(Object patchDir)
{
this.patchDir = patchDir;
}
public String getOriginalPrefix()
{
return originalPrefix;
}
public void setOriginalPrefix(String originalPrefix)
{
this.originalPrefix = originalPrefix;
}
public String getChangedPrefix()
{
return changedPrefix;
}
public void setChangedPrefix(String changedPrefix)
{
this.changedPrefix = changedPrefix;
}
}