diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java index 9f508effe6..80fe8907ac 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java @@ -35,6 +35,9 @@ package sonia.scm.repository; //~--- JDK imports ------------------------------------------------------------ +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; /** @@ -42,4 +45,15 @@ import javax.xml.bind.annotation.XmlRootElement; * @author Sebastian Sdorra */ @XmlRootElement(name = "config") -public class GitConfig extends SimpleRepositoryConfig {} +@XmlAccessorType(XmlAccessType.FIELD) +public class GitConfig extends SimpleRepositoryConfig { + + @XmlElement(name = "gc-expression") + private String gcExpression; + + public String getGcExpression() + { + return gcExpression; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java new file mode 100644 index 0000000000..f6d1b85fdf --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitGcTask.java @@ -0,0 +1,149 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.repository; + +import com.google.common.base.Stopwatch; +import com.google.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import org.eclipse.jgit.api.GarbageCollectCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Executes git gc on every git repository. Statistics of the gc process are logged to the info level. The task is + * disabled by default and must be enabled through the global git configuration. + * + * @author Sebastian Sdorra + * @since 1.47 + */ +public class GitGcTask implements Runnable { + + private static final String SP = System.getProperty("line.seperator", "\n"); + + private static final Logger logger = LoggerFactory.getLogger(GitGcTask.class); + + private final RepositoryManager repositoryManager; + private final RepositoryDirectoryHandler repositoryHandler; + + @Inject + public GitGcTask(RepositoryManager repositoryManager) + { + this.repositoryManager = repositoryManager; + this.repositoryHandler = (RepositoryDirectoryHandler) repositoryManager.getHandler(GitRepositoryHandler.TYPE_NAME); + } + + @Override + public void run() + { + for (Repository repository : repositoryManager.getAll()) + { + handle(repository); + } + } + + private void handle(Repository repository){ + if (GitRepositoryHandler.TYPE_NAME.equals(repository.getType())) + { + if (repository.isValid() && repository.isHealthy()) + { + logger.info("start git gc for repository {}", repository.getName()); + Stopwatch sw = Stopwatch.createStarted(); + gc(repository); + logger.debug("gc of repository {} has finished after {}", repository.getName(), sw.stop()); + } + else + { + logger.debug("skip non valid/healthy repository {}", repository.getName()); + } + } + else + { + logger.trace("skip non git repository {}", repository.getName()); + } + } + + private void appendProperties(StringBuilder buffer, Properties properties){ + for (Map.Entry entry : properties.entrySet()){ + buffer.append(SP).append(" - ").append(entry.getKey()).append(" = ").append(entry.getValue()); + } + } + + private String message(Repository repository, Properties statistics, String span){ + StringBuilder buffer = new StringBuilder("gc statistics for "); + buffer.append(repository.getName()).append(" ").append(span).append(" execution:"); + appendProperties(buffer, statistics); + return buffer.toString(); + } + + private void statistics(Repository repository, GarbageCollectCommand gcc) throws GitAPIException { + Properties properties = gcc.getStatistics(); + logger.info(message(repository, properties, "before")); + } + + private void execute(Repository repository, GarbageCollectCommand gcc) throws GitAPIException { + Properties properties = gcc.call(); + logger.info(message(repository, properties, "after")); + } + + private void gc(Repository repository){ + File file = repositoryHandler.getDirectory(repository); + Git git = null; + try { + git = Git.open(file); + GarbageCollectCommand gcc = git.gc(); + // print statistics before execution, because it looks like + // jgit returns the statistics after gc has finished + statistics(repository, gcc); + execute(repository, gcc); + } + catch (IOException ex) + { + logger.warn("failed to open git repository", ex); + } + catch (GitAPIException ex) + { + logger.warn("failed running git gc command", ex); + } + finally + { + if (git != null){ + git.close(); + } + } + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java index 713e589f8c..d919eacb68 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitRepositoryHandler.java @@ -35,6 +35,7 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- +import com.google.common.base.Strings; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -51,6 +52,15 @@ import sonia.scm.util.AssertUtil; import java.io.File; import java.io.IOException; +import java.util.Timer; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.schedule.Scheduler; +import sonia.scm.schedule.Task; /** * @@ -74,12 +84,18 @@ public class GitRepositoryHandler /** Field description */ public static final String TYPE_NAME = "git"; + + private static final Logger logger = LoggerFactory.getLogger(GitRepositoryHandler.class); /** Field description */ public static final Type TYPE = new RepositoryType(TYPE_NAME, TYPE_DISPLAYNAME, GitRepositoryServiceProvider.COMMANDS); + private static final Object LOCK = new Object(); + + private Task task; + //~--- constructors --------------------------------------------------------- /** @@ -88,15 +104,48 @@ public class GitRepositoryHandler * * @param storeFactory * @param fileSystem + * @param scheduler */ @Inject - public GitRepositoryHandler(StoreFactory storeFactory, FileSystem fileSystem) + public GitRepositoryHandler(StoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler) { super(storeFactory, fileSystem); + this.scheduler = scheduler; } //~--- get methods ---------------------------------------------------------- + @Override + public void init(SCMContextProvider context) + { + super.init(context); + scheduleGc(); + } + + @Override + public void setConfig(GitConfig config) + { + super.setConfig(config); + scheduleGc(); + } + + private void scheduleGc() + { + synchronized (LOCK){ + if ( task != null ){ + logger.debug("cancel existing git gc task"); + task.cancel(); + task = null; + } + String exp = getConfig().getGcExpression(); + if (!Strings.isNullOrEmpty(exp)) + { + logger.info("schedule git gc task with expression {}", exp); + task = scheduler.schedule(exp, GitGcTask.class); + } + } + } + /** * Method description * @@ -314,4 +363,6 @@ public class GitRepositoryHandler { return new File(directory, DIRECTORY_REFS).exists(); } + + private final Scheduler scheduler; } diff --git a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js b/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js index cb0aa9f16c..510b6aa97c 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js +++ b/scm-plugins/scm-git-plugin/src/main/resources/sonia/scm/git.config.js @@ -36,10 +36,12 @@ Sonia.git.ConfigPanel = Ext.extend(Sonia.config.SimpleConfigForm, { // labels titleText: 'Git Settings', repositoryDirectoryText: 'Repository directory', + gcExpressionText: 'Git GC Cron Expression', disabledText: 'Disabled', // helpTexts repositoryDirectoryHelpText: 'Location of the Git repositories.', + gcExpressionHelpText: 'Quartz Cron Expression to run git gc in intervals.', disabledHelpText: 'Enable or disable the Git plugin.\n\ Note you have to reload the page, after changing this value.', @@ -54,6 +56,12 @@ Sonia.git.ConfigPanel = Ext.extend(Sonia.config.SimpleConfigForm, { fieldLabel: this.repositoryDirectoryText, helpText: this.repositoryDirectoryHelpText, allowBlank : false + },{ + xtype: 'textfield', + name: 'gc-expression', + fieldLabel: this.gcExpressionText, + helpText: this.gcExpressionHelpText, + allowBlank : true },{ xtype: 'checkbox', name: 'disabled', diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java index eb9646ffe8..b6ee30a23a 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitRepositoryHandlerTest.java @@ -43,14 +43,22 @@ import static org.junit.Assert.*; //~--- JDK imports ------------------------------------------------------------ import java.io.File; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.schedule.Scheduler; /** * * @author Sebastian Sdorra */ +@RunWith(MockitoJUnitRunner.class) public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase { + @Mock + private Scheduler scheduler; + /** * Method description * @@ -90,7 +98,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase File directory) { GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory, - new DefaultFileSystem()); + new DefaultFileSystem(), scheduler); repositoryHandler.init(contextProvider);