[java-identity-provider] 01/04: IDP-1682 Move the plugins webapp to a common location 1/many
Rod Widdowson
rdw at steadingsoftware.com
Sun Oct 4 13:51:48 UTC 2020
This is an automated email from the git hooks/post-receive script.
rdw pushed a commit to branch main
in repository java-identity-provider.
View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=16460b1039c90a97b585ddeac5e02ba476a198f6
commit 16460b1039c90a97b585ddeac5e02ba476a198f6
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Sat Oct 3 17:19:15 2020 +0100
IDP-1682 Move the plugins webapp to a common location 1/many
https://issues.shibboleth.net/jira/browse/IDP-1682
Log the contents of what is added to the webapp folder to a property file.
---
.../shibboleth/idp/installer/CopyDistribution.java | 2 +-
.../idp/installer/plugin/impl/PluginInstaller.java | 135 ++++++------
.../plugin/impl/PluginInstallerArguments.java | 2 +-
.../plugin/impl/PluginInstallerSupport.java | 238 +++++++++++++++++++++
.../idp/installer/plugin/impl/BasePluginTest.java | 2 +-
.../idp/installer/plugin/impl/PluginCLITest.java | 2 +-
6 files changed, 314 insertions(+), 67 deletions(-)
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/CopyDistribution.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/CopyDistribution.java
index 049964139..34688209f 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/CopyDistribution.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/CopyDistribution.java
@@ -91,7 +91,7 @@ public final class CopyDistribution extends AbstractInitializableComponent {
*/
protected void deleteOld() {
delete(installerProps.getTargetDir().resolve("bin").resolve("lib"), null);
- delete(installerProps.getTargetDir().resolve("dist"), "edit-webapp-*/**");
+ delete(installerProps.getTargetDir().resolve("dist"), "edit-webapp-*/** plugin-contents/**");
delete(installerProps.getTargetDir().resolve("doc"), null);
final Path system = installerProps.getTargetDir().resolve("system");
if (Files.exists(system)) {
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
index 1fca3c199..234955312 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
@@ -18,6 +18,7 @@
package net.shibboleth.idp.installer.plugin.impl;
import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -27,11 +28,9 @@ import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.DirectoryStream;
-import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
@@ -40,8 +39,8 @@ import java.util.Optional;
import java.util.Properties;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
-import java.util.Set;
import java.util.ServiceLoader.Provider;
+import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -57,7 +56,6 @@ import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.http.client.HttpClient;
import org.apache.tools.ant.BuildException;
-import org.apache.tools.ant.taskdefs.Copy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -71,6 +69,7 @@ import net.shibboleth.idp.installer.plugin.impl.TrustStore.Signature;
import net.shibboleth.idp.module.IdPModule;
import net.shibboleth.idp.module.ModuleContext;
import net.shibboleth.idp.plugin.IdPPlugin;
+import net.shibboleth.idp.plugin.PluginVersion;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.collection.Pair;
@@ -123,6 +122,9 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
/** What to use to download things. */
private HttpClient httpClient;
+
+ /** Files that were copied - to handle rollback. */
+ @Nonnull private List<Path> copiedFiles = new ArrayList<>();
/** Set IdP Home.
* @param home Where we are working from
@@ -167,22 +169,13 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
httpClient = Constraint.isNotNull(what, "HttpClient should be non-null");
}
- /** Return the canonical path.
- * @param from the path we get given
- * @return the canonicalized one
- * @throws IOException as from {@link File#getCanonicalFile()}
- */
- private static Path canonicalPath(final Path from) throws IOException {
- return from.toFile().getCanonicalFile().toPath();
- }
-
/** Check that the provide path is inside {@link #idpHome}.
* @param to the path to check.
* @throws BuildException if it isn't
*/
private void policeTo(final Path to) throws BuildException {
try {
- final Path canonicalTo = canonicalPath(to);
+ final Path canonicalTo = PluginInstallerSupport.canonicalPath(to);
if (!canonicalTo.startsWith(idpHome)) {
LOG.error("File destination {} ({}) was illegal (not inside {}", to, canonicalTo, idpHome);
throw new BuildException("Illegal file destination");
@@ -199,7 +192,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
*/
private void policeFrom(final Path from) throws BuildException {
try {
- final Path canonicalFrom = canonicalPath(from);
+ final Path canonicalFrom = PluginInstallerSupport.canonicalPath(from);
if (!canonicalFrom.startsWith(distribution)) {
LOG.error("File source {} ({}) was illegal (not inside {}", from, canonicalFrom, distribution);
throw new BuildException("Illegal file source");
@@ -252,12 +245,12 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
final Path myWebApp = idpHome.resolve("dist").resolve("edit-webapp-" + pluginId);
- InstallerSupport.setReadOnly(myWebApp, false);
- deleteTree(myWebApp);
+ uninstallOld(myWebApp);
installWebapp(myWebApp);
installFiles();
downloadExternals();
InstallerSupport.setReadOnly(myWebApp, true);
+ saveCopiedFiles();
final BuildWar builder = new BuildWar(idpHome);
try {
@@ -276,8 +269,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
LOG.error("Plugin {} had no jars installed.", pluginId);
return;
}
- InstallerSupport.setReadOnly(myWebApp, false);
- deleteTree(myWebApp);
+ uninstallOld(myWebApp);
final BuildWar builder = new BuildWar(idpHome);
try {
builder.initialize();
@@ -457,9 +449,45 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
*/
private void installWebapp(final Path myWebApp) throws BuildException {
final Path from = distribution.resolve("edit-webapp");
- LOG.debug("Copying distribution from {} to {}", from, myWebApp);
- final Copy copy = InstallerSupport.getCopyTask(from, myWebApp);
- copy.execute();
+ if (PluginInstallerSupport.detectDuplicates(from, myWebApp)) {
+ throw new BuildException("Install would overwrite filess");
+ }
+ PluginInstallerSupport.copyWithLogging(from, myWebApp, copiedFiles);
+ }
+
+ /** Uninstall the old version of the plugin.
+ * @param myWebApp where to delete */
+ private void uninstallOld(final Path myWebApp) {
+ InstallerSupport.setReadOnly(myWebApp, false);
+ PluginInstallerSupport.deleteTree(myWebApp);
+ }
+
+ /** Stream the copy list to a property file and empty it.
+ * @throws BuildException If we hit an IO exception
+ */
+ private void saveCopiedFiles() throws BuildException {
+ try {
+ final Path parent = idpHome.resolve("dist").resolve("plugin-contents");
+ Files.createDirectories(parent);
+ final Properties props = new Properties(1+copiedFiles.size());
+ props.setProperty("idp.plugin.version",
+ new PluginVersion(description.getMajorVersion(),
+ description.getMinorVersion(),
+ description.getPatchVersion()).toString());
+ int count = 1;
+ for (final Path p: copiedFiles) {
+ props.setProperty("idp.plugin.file."+Integer.toString(count++),
+ PluginInstallerSupport.canonicalPath(p).toString());
+ }
+ final File outFile = parent.resolve(pluginId).toFile();
+ try (final BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outFile))) {
+ props.store(out, "Files Copied " + Instant.now());
+ }
+ copiedFiles = new ArrayList<>();
+ } catch (final IOException e) {
+ LOG.error("Error saving list of copied files.", e);
+ throw new BuildException(e);
+ }
}
/** Method to download a zip file to the {{@link #downloadDirectory}.
@@ -553,7 +581,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
LOG.error("No contents unpacked from {}", fullName);
throw new BuildException("Distro was empty");
}
- distribution = canonicalPath(contents.next());
+ distribution = PluginInstallerSupport.canonicalPath(contents.next());
if (contents.hasNext()) {
LOG.error("Too many packages in distributions {}", fullName);
throw new BuildException("Too many packages in distributions");
@@ -602,27 +630,27 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
* @throws BuildException if badness is detected.
*/
private void setupPluginId() throws BuildException {
- final File propertyFile = distribution.resolve("bootstrap").resolve("plugin.properties").toFile();
+ final File propertyFile = distribution.resolve("bootstrap").resolve("id.property").toFile();
if (!propertyFile.exists()) {
LOG.error("Could not locate identity of plugin. "
- + "Identity file 'bootstrap/plugin.properties' not present in plugin distribution");
+ + "Identity file 'bootstrap/id.property' not present in plugin distribution.");
throw new BuildException("Could not locate identity of plugin");
}
try (final InputStream inStream = new BufferedInputStream(new FileInputStream(propertyFile))) {
final Properties idProperties = new Properties();
idProperties.load(inStream);
- final String id = StringSupport.trimOrNull(idProperties.getProperty("plugin.id"));
+ final String id = StringSupport.trimOrNull(idProperties.getProperty("pluginid"));
if (id == null) {
- LOG.error("Identity file 'bootstrap/plugin.properties' did not contain 'plugin.id' property");
- throw new BuildException("No property in bootstrap/plugin.properties file");
+ LOG.error("Identity property file 'bootstrap/id.property' did not contain 'pluginid' property");
+ throw new BuildException("No property in ID file");
}
if (pluginId != null && !pluginId.equals(id)) {
- LOG.error("Downloaded plugin id {} conflicts with provided id {}", id, pluginId);
+ LOG.error("Downloaded plugin id {} overriden by provided id {}", id, pluginId);
} else {
setPluginId(id);
}
} catch (final IOException e) {
- LOG.error("Could not load plugin identity file 'bootstrap/plugin.properties'", e);
+ LOG.error("Could not load plugin identity file 'bootstrap/id.property'", e);
throw new BuildException(e);
}
}
@@ -678,7 +706,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
throw new ComponentInitializationException("Idp Home should be set");
}
try {
- idpHome = canonicalPath(idpHome);
+ idpHome = PluginInstallerSupport.canonicalPath(idpHome);
} catch (final IOException e) {
LOG.error("Could not canonicalize idp home", e);
throw new ComponentInitializationException(e);
@@ -715,39 +743,20 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
}
}
- /** Delete a directory tree.
- * @param directory what to delete
- */
- public static void deleteTree(@Nullable final Path directory) {
- if (directory == null || !Files.exists(directory)) {
- return;
- }
- LOG.debug("Deleting directory {}", directory);
- try {
- Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
- @Override
- public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
- Files.delete(file);
- return FileVisitResult.CONTINUE;
- }
- @Override
- public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
- if (exc != null) {
- throw exc;
- }
- Files.delete(dir);
- return FileVisitResult.CONTINUE;
- }
- });
- } catch (final IOException e) {
- LOG.error("Couldn't delete {}", directory, e);
- }
- }
-
/** {@inheritDoc} */
public void close() {
- deleteTree(downloadDirectory);
- deleteTree(unpackDirectory);
+ PluginInstallerSupport.deleteTree(downloadDirectory);
+ PluginInstallerSupport.deleteTree(unpackDirectory);
+ for (final Path p : copiedFiles) {
+ try {
+ if (Files.exists(p)) {
+ Files.delete(p);
+ }
+ } catch (final IOException e) {
+ p.toFile().deleteOnExit();
+ LOG.warn("Failed to delete {}", p, e);
+ }
+ }
}
}
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerArguments.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerArguments.java
index fe98a0fa8..2cfb6e8fc 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerArguments.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerArguments.java
@@ -272,7 +272,7 @@ public class PluginInstallerArguments extends AbstractIdPHomeAwareCommandLineArg
return OperationType.INSTALLREMOTE;
}
} catch (final MalformedURLException e) {
- getLog().trace("urg");
+ // It's OK
}
// Must be a file
final File inputAsFile = new File(input);
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerSupport.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerSupport.java
new file mode 100644
index 000000000..fcbda89b0
--- /dev/null
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstallerSupport.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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 net.shibboleth.idp.installer.plugin.impl;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.tools.ant.BuildException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+
+/**
+ * Support for copying files during plugin manipulation.
+ */
+public final class PluginInstallerSupport {
+
+ /** Class logger. */
+ @Nonnull
+ private static final Logger LOG = LoggerFactory.getLogger(PluginInstallerSupport.class);
+
+ /** Constructor. */
+ private PluginInstallerSupport() {
+ }
+
+ /** Return the canonical path.
+ * @param from the path we get given
+ * @return the canonicalized one
+ * @throws IOException as from {@link File#getCanonicalFile()}
+ */
+ static Path canonicalPath(final Path from) throws IOException {
+ return from.toFile().getCanonicalFile().toPath();
+ }
+
+ /** Delete a directory tree.
+ * @param directory what to delete
+ */
+ public static void deleteTree(@Nullable final Path directory) {
+ if (directory == null || !Files.exists(directory)) {
+ return;
+ }
+ LOG.debug("Deleting directory {}", directory);
+ try {
+ Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+ @Override
+ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
+ if (exc != null) {
+ throw exc;
+ }
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (final IOException e) {
+ LOG.error("Couldn't delete {}", directory, e);
+ }
+ }
+
+ /** Traverse "from" looking to see if any of the files are already in "to".
+ * @param from source directory
+ * @param to target directory
+ * @return true if there was a match
+ * @throws BuildException if anything threw and {@link IOException}
+ */
+ public static boolean detectDuplicates(final Path from, final Path to) throws BuildException {
+
+ if (to == null || !Files.exists(to)) {
+ return false;
+ }
+ final NameClashVisitor detector = new NameClashVisitor(from, to);
+ LOG.debug("Walking {}, looking for a name clash in {}", from, to);
+ try {
+ Files.walkFileTree(from, detector);
+ } catch (final IOException e) {
+ LOG.error("Failed during duplicate detection:", e);
+ throw new BuildException(e);
+ }
+ return detector.wasNameClash();
+ }
+
+ /** Copy a directory tree and keep a log of what has changed.
+ * @param from source directory
+ * @param to target directory
+ * @param pathsCopied the list of files copied up (including if there was a failure)
+ * @throws BuildException from the copy
+ */
+ public static void copyWithLogging(final Path from,
+ final Path to, @Live final List<Path> pathsCopied) throws BuildException {
+ if (from == null || !Files.exists(from)) {
+ return;
+ }
+ LOG.debug("Copying from {} to {}", from, to);
+ final LoggingVisitor visitor = new LoggingVisitor(from, to);
+ try {
+ Files.walkFileTree(from, visitor);
+ } catch (final IOException e) {
+ pathsCopied.addAll(visitor.getCopiedList());
+ LOG.error("Error copying files from {} to {}", from, to, e);
+ throw new BuildException(e);
+ }
+ pathsCopied.addAll(visitor.getCopiedList());
+ }
+
+ /**
+ * A @{link {@link FileVisitor} which detects (and logs) whether a copy would overwrite.
+ */
+ private static final class NameClashVisitor extends SimpleFileVisitor<Path> {
+ /** did we find a duplicate. */
+ private boolean nameClash;
+
+ /** Path we are traversing. */
+ private final Path from;
+
+ /** Path where we check for Duplicates. */
+ private final Path to;
+ /**
+ * Constructor.
+ *
+ * @param fromDir Path we are traversing
+ * @param toDir Path where we check for Duplicates
+ */
+ public NameClashVisitor(final Path fromDir, final Path toDir) {
+ from = fromDir;
+ to = toDir;
+ }
+
+ @Override
+ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+ final Path relFile = from.relativize(file);
+ final Path toFile = to.resolve(relFile);
+ if (Files.exists(toFile)) {
+ nameClash = true;
+ LOG.warn("{} already exists", toFile);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ /** did we find a name clash?
+ * @return whether we found a name clash.
+ */
+ public boolean wasNameClash() {
+ return nameClash;
+ }
+ }
+
+ /**
+ * A @{link {@link FileVisitor} which detects (and logs) whether a copy would overwrite.
+ */
+ private static final class LoggingVisitor extends SimpleFileVisitor<Path> {
+ /** How what files have we copied? */
+ private final List<Path> copiedFiles = new ArrayList<>();
+
+ /** Path we are traversing. */
+ private final Path from;
+
+ /** Path where we check for Duplicates. */
+ private final Path to;
+ /**
+ * Constructor.
+ *
+ * @param fromDir Path we are traversing
+ * @param toDir Path where we check for Duplicates
+ */
+ public LoggingVisitor(final Path fromDir, final Path toDir) {
+ from = fromDir;
+ to = toDir;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
+ final Path relDir = from.relativize(dir);
+ final Path toDir = to.resolve(relDir);
+ if (!Files.exists(toDir)) {
+ LOG.trace("Creating directory {}", toDir);
+ Files.createDirectory(toDir);
+ }
+ return FileVisitResult.CONTINUE;
+ };
+
+ @Override
+ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+ final Path relFile = from.relativize(file);
+ final Path toFile = to.resolve(relFile);
+ copiedFiles.add(toFile);
+ try(final InputStream in = new BufferedInputStream(new FileInputStream(file.toFile()));
+ final OutputStream out = new BufferedOutputStream(new FileOutputStream(toFile.toFile()))) {
+ in.transferTo(out);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ /** did we find a name clash?
+ * @return whether we found a name clash.
+ */
+ public List<Path> getCopiedList() {
+ return copiedFiles;
+ }
+ }
+
+}
diff --git a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/BasePluginTest.java b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/BasePluginTest.java
index de117cd9c..e74af4040 100644
--- a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/BasePluginTest.java
+++ b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/BasePluginTest.java
@@ -71,7 +71,7 @@ public class BasePluginTest {
return;
}
InstallerSupport.setReadOnly(idpHome, false);
- PluginInstaller.deleteTree(idpHome);
+ PluginInstallerSupport.deleteTree(idpHome);
}
protected Path getIdpHome() {
diff --git a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/PluginCLITest.java b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/PluginCLITest.java
index 9e427c681..0ee11a716 100644
--- a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/PluginCLITest.java
+++ b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/impl/PluginCLITest.java
@@ -111,7 +111,7 @@ public class PluginCLITest extends BasePluginTest {
AbstractCommandLine.RC_OK);
} finally {
if (unpack != null) {
- PluginInstaller.deleteTree(unpack);
+ PluginInstallerSupport.deleteTree(unpack);
}
}
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list