From 5991fd9ccc2c1635668cc4b8e4bae181d09b460e Mon Sep 17 00:00:00 2001
From: jvergara <jvergara@localhost>
Date: Thu, 19 Nov 2009 23:53:25 +0000
Subject: [PATCH] Fix for issue 3551 (Consider proposing to initialize a topology when the user imports an LDIF in a replicated base DN) When the user imports a file in a backend with replicated suffixes, the possibility of initializing the whole replication topology using the replication protocol is offered to the user.

---
 opends/src/guitools/org/opends/guitools/controlpanel/ui/ConfirmInitializeAndImportDialog.java |  315 +++++++++++++++++++++++++++++++++++
 opends/src/messages/messages/admin_tool.properties                                            |   21 ++
 opends/src/server/org/opends/server/tools/dsreplication/ReplicationCliMain.java               |   38 +++
 opends/src/guitools/org/opends/guitools/controlpanel/ui/ImportLDIFPanel.java                  |  156 +++++++++++++++++
 4 files changed, 521 insertions(+), 9 deletions(-)

diff --git a/opends/src/guitools/org/opends/guitools/controlpanel/ui/ConfirmInitializeAndImportDialog.java b/opends/src/guitools/org/opends/guitools/controlpanel/ui/ConfirmInitializeAndImportDialog.java
new file mode 100644
index 0000000..47a48e7
--- /dev/null
+++ b/opends/src/guitools/org/opends/guitools/controlpanel/ui/ConfirmInitializeAndImportDialog.java
@@ -0,0 +1,315 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2009 Sun Microsystems, Inc.
+ */
+
+package org.opends.guitools.controlpanel.ui;
+
+import static org.opends.messages.AdminToolMessages.*;
+
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JPanel;
+
+import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
+import org.opends.guitools.controlpanel.event.ConfigurationChangeEvent;
+import org.opends.guitools.controlpanel.util.Utilities;
+import org.opends.messages.Message;
+
+/**
+ * Dialog used to inform the user that there are unsaved changes in a panel.
+ * It proposes the user to save the changes, do not save them or cancel the
+ * action that make the dialog appear (for instance when the user is editing
+ * an entry and clicks on another node, this dialog appears).
+ *
+ */
+public class ConfirmInitializeAndImportDialog extends GenericDialog
+{
+  /**
+   * The different input that the user can provide.
+   *
+   */
+  public enum Result
+  {
+    /**
+     * The user asks to do the import and then the initialization.
+     */
+    INITIALIZE_ALL,
+    /**
+     * The user asks to only do the import locally.
+     */
+    IMPORT_ONLY,
+    /**
+     * The user asks to cancel the operation that made this dialog to appear.
+     */
+    CANCEL
+  }
+  private static final long serialVersionUID = -442311801035162311L;
+
+  /**
+   * Constructor of the dialog.
+   * @param parentDialog the parent dialog.
+   * @param info the control panel info.
+   */
+  public ConfirmInitializeAndImportDialog(Component parentDialog,
+      ControlPanelInfo info)
+  {
+    super(Utilities.getFrame(parentDialog), getPanel(info));
+    Utilities.centerGoldenMean(this, parentDialog);
+    getRootPane().setDefaultButton(
+        ((ConfirmInitializeAndImportPanel)panel).initializeAllButton);
+    setModal(true);
+  }
+
+  /**
+   * Sets the message to be displayed in this dialog.
+   * @param title the title of the message.
+   * @param details the details of the message.
+   */
+  public void setMessage(Message title, Message details)
+  {
+    panel.updateConfirmationPane(panel.errorPane, title,
+        ColorAndFontConstants.errorTitleFont, details,
+        ColorAndFontConstants.defaultFont);
+    invalidate();
+    pack();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public void setVisible(boolean visible)
+  {
+    if (visible)
+    {
+      ((ConfirmInitializeAndImportPanel)panel).result = Result.CANCEL;
+    }
+    super.setVisible(visible);
+  }
+
+  /**
+   * Returns the option the user gave when closing this dialog.
+   * @return the option the user gave when closing this dialog.
+   */
+  public Result getResult()
+  {
+    return ((ConfirmInitializeAndImportPanel)panel).result;
+  }
+
+  /**
+   * Creates the panel to be displayed inside the dialog.
+   * @param info the control panel info.
+   * @return the panel to be displayed inside the dialog.
+   */
+  private static StatusGenericPanel getPanel(ControlPanelInfo info)
+  {
+    ConfirmInitializeAndImportPanel panel =
+      new ConfirmInitializeAndImportPanel();
+    panel.setInfo(info);
+    return panel;
+  }
+
+  /**
+   * The panel to be displayed inside the dialog.
+   *
+   */
+  private static class ConfirmInitializeAndImportPanel
+  extends StatusGenericPanel
+  {
+    private static final long serialVersionUID = -9890116762604059L;
+
+    private JButton initializeAllButton;
+    private JButton importOnlyButton;
+    private JButton cancelButton;
+
+    private Result result;
+
+    /**
+     * Default constructor.
+     *
+     */
+    public ConfirmInitializeAndImportPanel()
+    {
+      super();
+      GridBagConstraints gbc = new GridBagConstraints();
+      gbc.gridx = 0;
+      gbc.gridy = 0;
+      gbc.gridwidth = 1;
+      addErrorPane(gbc);
+      errorPane.setVisible(true);
+      gbc.gridy ++;
+      gbc.fill = GridBagConstraints.VERTICAL;
+      gbc.weighty = 1.0;
+      add(Box.createVerticalGlue(), gbc);
+      gbc.fill = GridBagConstraints.HORIZONTAL;
+//    The button panel
+      gbc.gridy ++;
+      gbc.weighty = 0.0;
+      gbc.insets = new Insets(0, 0, 0, 0);
+      add(createButtonsPanel(), gbc);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean requiresBorder()
+    {
+      return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean requiresScroll()
+    {
+      return false;
+    }
+
+    private JPanel createButtonsPanel()
+    {
+      JPanel buttonsPanel = new JPanel(new GridBagLayout());
+      buttonsPanel.setOpaque(true);
+      buttonsPanel.setBackground(ColorAndFontConstants.greyBackground);
+      GridBagConstraints gbc = new GridBagConstraints();
+      gbc.gridx = 0;
+      gbc.gridy = 0;
+      gbc.anchor = GridBagConstraints.WEST;
+      gbc.fill = GridBagConstraints.HORIZONTAL;
+      gbc.gridwidth = 1;
+      gbc.gridy = 0;
+      gbc.weightx = 1.0;
+      gbc.gridx ++;
+      buttonsPanel.add(Box.createHorizontalStrut(150));
+      buttonsPanel.add(Box.createHorizontalGlue(), gbc);
+
+      initializeAllButton = Utilities.createButton(
+          INFO_CTRL_PANEL_INITIALIZE_ALL_BUTTON_LABEL.get());
+      initializeAllButton.setOpaque(false);
+      gbc.insets = new Insets(10, 10, 10, 10);
+      gbc.weightx = 0.0;
+      gbc.gridx ++;
+      buttonsPanel.add(initializeAllButton, gbc);
+      initializeAllButton.addActionListener(new ActionListener()
+      {
+        public void actionPerformed(ActionEvent ev)
+        {
+          result = Result.INITIALIZE_ALL;
+          cancelClicked();
+        }
+      });
+
+      gbc.gridx ++;
+      importOnlyButton = Utilities.createButton(
+          INFO_CTRL_PANEL_IMPORT_ONLY_BUTTON_LABEL.get());
+      importOnlyButton.setOpaque(false);
+      gbc.gridx ++;
+      gbc.insets.left = 0;
+      gbc.insets.right = 10;
+      buttonsPanel.add(importOnlyButton, gbc);
+      importOnlyButton.addActionListener(new ActionListener()
+      {
+        /**
+         * {@inheritDoc}
+         */
+        public void actionPerformed(ActionEvent ev)
+        {
+          result = Result.IMPORT_ONLY;
+          cancelClicked();
+        }
+      });
+
+      cancelButton = Utilities.createButton(
+          INFO_CTRL_PANEL_CANCEL_BUTTON_LABEL.get());
+      cancelButton.setOpaque(false);
+      gbc.insets.right = 10;
+      gbc.gridx ++;
+      buttonsPanel.add(cancelButton, gbc);
+      cancelButton.addActionListener(new ActionListener()
+      {
+        /**
+         * {@inheritDoc}
+         */
+        public void actionPerformed(ActionEvent ev)
+        {
+          result = Result.CANCEL;
+          cancelClicked();
+        }
+      });
+
+      buttonsPanel.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0,
+          ColorAndFontConstants.defaultBorderColor));
+
+      return buttonsPanel;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Component getPreferredFocusComponent()
+    {
+      return initializeAllButton;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void okClicked()
+    {
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Message getTitle()
+    {
+      return INFO_CTRL_PANEL_CONFIRM_INITIALIZE_TITLE.get();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void configurationChanged(ConfigurationChangeEvent ev)
+    {
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public GenericDialog.ButtonType getButtonType()
+    {
+      return GenericDialog.ButtonType.NO_BUTTON;
+    }
+  }
+}
+
diff --git a/opends/src/guitools/org/opends/guitools/controlpanel/ui/ImportLDIFPanel.java b/opends/src/guitools/org/opends/guitools/controlpanel/ui/ImportLDIFPanel.java
index 25ebf7e..caf520e 100644
--- a/opends/src/guitools/org/opends/guitools/controlpanel/ui/ImportLDIFPanel.java
+++ b/opends/src/guitools/org/opends/guitools/controlpanel/ui/ImportLDIFPanel.java
@@ -54,7 +54,9 @@
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 
+import org.opends.admin.ads.util.ConnectionUtils;
 import org.opends.guitools.controlpanel.datamodel.BackendDescriptor;
+import org.opends.guitools.controlpanel.datamodel.BaseDNDescriptor;
 import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
 import org.opends.guitools.controlpanel.datamodel.ServerDescriptor;
 import org.opends.guitools.controlpanel.event.BrowseActionListener;
@@ -62,8 +64,14 @@
 import org.opends.guitools.controlpanel.task.Task;
 import org.opends.guitools.controlpanel.util.Utilities;
 import org.opends.messages.Message;
+import org.opends.quicksetup.ui.UIFactory;
 import org.opends.quicksetup.util.Utils;
 import org.opends.server.tools.ImportLDIF;
+import org.opends.server.tools.dsreplication.ReplicationCliArgumentParser;
+import org.opends.server.tools.dsreplication.ReplicationCliException;
+import org.opends.server.tools.dsreplication.ReplicationCliMain;
+import org.opends.server.types.DN;
+import org.opends.server.util.cli.CommandBuilder;
 
 /**
  * The panel where the user can import the contents of an LDIF file to the
@@ -640,18 +648,69 @@
         task.canLaunch(newTask, errors);
       }
       boolean confirmed = true;
+      boolean initializeAll = false;
       if (errors.isEmpty())
       {
-        if (overwrite.isSelected())
+        Set<DN> replicatedBaseDNs = getReplicatedBaseDNs();
+        boolean canInitialize =
+          !replicatedBaseDNs.isEmpty() && isServerRunning();
+        if (overwrite.isSelected() && !canInitialize)
         {
           confirmed = displayConfirmationDialog(
               INFO_CTRL_PANEL_CONFIRMATION_REQUIRED_SUMMARY.get(),
               INFO_CTRL_PANEL_CONFIRMATION_IMPORT_LDIF_DETAILS.get(
-                  backends.getSelectedItem().toString()));
+                  backendName));
+        }
+        else if (!overwrite.isSelected() && canInitialize)
+        {
+          ArrayList<String> dns = new ArrayList<String>();
+          for (DN dn : replicatedBaseDNs)
+          {
+            dns.add(dn.toString());
+          }
+          initializeAll = displayConfirmationDialog(
+              INFO_CTRL_PANEL_CONFIRMATION_REQUIRED_SUMMARY.get(),
+              INFO_CTRL_PANEL_CONFIRMATION_INITIALIZE_ALL_DETAILS.get(
+                  Utilities.getStringFromCollection(dns, "<br>")));
+        }
+        else if (overwrite.isSelected() && canInitialize)
+        {
+          ArrayList<String> dns = new ArrayList<String>();
+          for (DN dn : replicatedBaseDNs)
+          {
+            dns.add(dn.toString());
+          }
+          ConfirmInitializeAndImportDialog dlg =
+            new ConfirmInitializeAndImportDialog(
+                Utilities.getParentDialog(this), getInfo());
+          dlg.setMessage(INFO_CTRL_PANEL_CONFIRM_INITIALIZE_TITLE.get(),
+          INFO_CTRL_PANEL_CONFIRMATION_INITIALIZE_ALL_AND_OVERWRITE_DETAILS.get(
+                  backendName, Utilities.getStringFromCollection(dns, "<br>")));
+          dlg.setModal(true);
+          dlg.setVisible(true);
+
+          ConfirmInitializeAndImportDialog.Result result = dlg.getResult();
+          switch (result)
+          {
+          case CANCEL:
+            confirmed = false;
+            break;
+          case INITIALIZE_ALL:
+            confirmed = true;
+            initializeAll = true;
+            break;
+          case IMPORT_ONLY:
+            confirmed = true;
+            initializeAll = false;
+            break;
+            default:
+              throw new RuntimeException("Unexpected result: "+result);
+          }
         }
       }
       if ((errors.isEmpty()) && confirmed)
       {
+        newTask.setInitializeAll(initializeAll);
         launchOperation(newTask,
             INFO_CTRL_PANEL_IMPORTING_LDIF_SUMMARY.get(
                 backends.getSelectedItem().toString()),
@@ -687,6 +746,30 @@
     super.cancelClicked();
   }
 
+  private Set<DN> getReplicatedBaseDNs()
+  {
+    Set<DN> baseDNs = new TreeSet<DN>();
+    String backendID = (String)backends.getSelectedItem();
+    if (backendID != null)
+    {
+      for (BackendDescriptor backend :
+        getInfo().getServerDescriptor().getBackends())
+      {
+        if (backendID.equalsIgnoreCase(backend.getBackendID()))
+        {
+          for (BaseDNDescriptor baseDN : backend.getBaseDns())
+          {
+            if (baseDN.getReplicaID() != -1)
+            {
+              baseDNs.add(baseDN.getDn());
+            }
+          }
+        }
+      }
+    }
+    return baseDNs;
+  }
+
   /**
    * The class that performs the import.
    *
@@ -695,6 +778,8 @@
   {
     private Set<String> backendSet;
     private String fileName;
+    private boolean initializeAll;
+    private Set<DN> replicatedBaseDNs;
 
     /**
      * The constructor of the task.
@@ -707,6 +792,12 @@
       backendSet = new HashSet<String>();
       backendSet.add((String)backends.getSelectedItem());
       fileName = file.getText();
+      replicatedBaseDNs = getReplicatedBaseDNs();
+    }
+
+    private void setInitializeAll(boolean initializeAll)
+    {
+      this.initializeAll = initializeAll;
     }
 
     /**
@@ -849,6 +940,10 @@
         {
           returnCode = ImportLDIF.mainImportLDIF(args, false, outPrintStream,
               errorPrintStream);
+          if (returnCode == 0 && initializeAll)
+          {
+            initializeAll();
+          }
         }
         else
         {
@@ -898,5 +993,62 @@
     {
       return backendSet;
     }
+
+    private void initializeAll() throws ReplicationCliException
+    {
+      ReplicationCliMain repl = new ReplicationCliMain(outPrintStream,
+          errorPrintStream, System.in);
+      getProgressDialog().appendProgressHtml(
+          UIFactory.HTML_SEPARATOR+"<br><br>");
+
+      String cmd = getCommandLineToInitializeAll();
+
+      getProgressDialog().appendProgressHtml(Utilities.applyFont(
+          INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_INITIALIZE_ALL.get().toString()+
+          "<br><b>"+cmd+"</b><br><br>",
+          ColorAndFontConstants.progressFont));
+
+      for (DN baseDN : replicatedBaseDNs)
+      {
+        Message msg = INFO_PROGRESS_INITIALIZING_SUFFIX.get(baseDN.toString(),
+            ConnectionUtils.getHostPort(getInfo().getDirContext()));
+        getProgressDialog().appendProgressHtml(Utilities.applyFont(
+            msg.toString()+"<br>", ColorAndFontConstants.progressFont));
+        repl.initializeAllSuffix(baseDN.toString(), getInfo().getDirContext(),
+            true);
+      }
+    }
+
+    private String getCommandLineToInitializeAll()
+    {
+      StringBuilder sb = new StringBuilder();
+      String cmdLineName = getCommandLinePath("dsreplication");
+      sb.append(cmdLineName);
+      ArrayList<String> args = new ArrayList<String>();
+      args.add(
+          ReplicationCliArgumentParser.INITIALIZE_ALL_REPLICATION_SUBCMD_NAME);
+      args.add("--hostName");
+      args.add(getInfo().getServerDescriptor().getHostname());
+      args.add("--port");
+      args.add(String.valueOf(
+          ConnectionUtils.getPort(getInfo().getDirContext())));
+      for (DN baseDN : replicatedBaseDNs)
+      {
+        args.add("--baseDN");
+        args.add(baseDN.toString());
+      }
+      args.add("--adminUID");
+      args.add("admin");
+      args.add("--adminPassword");
+      args.add(Utilities.OBFUSCATED_VALUE);
+      args.add("--trustAll");
+      args.add("--no-prompt");
+
+      for (String arg : args)
+      {
+        sb.append(" "+CommandBuilder.escapeValue(arg));
+      }
+      return sb.toString();
+    }
   };
 }
diff --git a/opends/src/messages/messages/admin_tool.properties b/opends/src/messages/messages/admin_tool.properties
index 4d3dbf6..6cd70f2 100644
--- a/opends/src/messages/messages/admin_tool.properties
+++ b/opends/src/messages/messages/admin_tool.properties
@@ -1788,10 +1788,29 @@
 MILD_ERR_CTRL_PANEL_SKIPS_FILE_REQUIRED=You must provide a value for the \
  skips file.
 #
-# Note that the following property contains line breaks in HTML format (<br>)
+# Note that the following three properties contain line breaks in HTML format
+# (<br>)
 #
 INFO_CTRL_PANEL_CONFIRMATION_IMPORT_LDIF_DETAILS=All the data in backend '%s' \
  will be overwritten.<br><br>Do you want to continue?
+INFO_CTRL_PANEL_CONFIRMATION_INITIALIZE_ALL_DETAILS=The following base DNs are \
+ replicated:<br>%s<br><br>In order replication to work, these base DNs must \
+ be initialized once the import of the LDIF is finished.<br><br>Do you want to \
+ initialize automatically the contents of the replicated base DNs in the \
+ remote servers once the import LDIF has finished?  Note that if you click \
+ 'Yes' all the data in the remote server base DNs will be overwritten.
+INFO_CTRL_PANEL_CONFIRMATION_INITIALIZE_ALL_AND_OVERWRITE_DETAILS=All the data \
+ in backend '%s' will be overwritten.<br><br>The following base DNs are \
+ replicated:<br>%s<br><br>In order replication to work, these base DNs must \
+ be initialized once the import of the LDIF is finished.<br><br>You can choose \
+ to initialize automatically the contents of the replicated base DNs in the \
+ remote servers once the import LDIF has finished.  Note that if you choose \
+ to initialize all the data in the remote server base DNs will be overwritten.
+INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_INITIALIZE_ALL=Equivalent command to \
+ initialize remote servers:
+INFO_CTRL_PANEL_CONFIRM_INITIALIZE_TITLE=Confirmation Required
+INFO_CTRL_PANEL_INITIALIZE_ALL_BUTTON_LABEL=Import and Initialize
+INFO_CTRL_PANEL_IMPORT_ONLY_BUTTON_LABEL=Import Only
 INFO_CTRL_PANEL_IMPORTING_LDIF_SUMMARY=Importing to backend '%s'...
 INFO_CTRL_PANEL_IMPORTING_LDIF_SUCCESSFUL_SUMMARY=Import Complete
 INFO_CTRL_PANEL_IMPORTING_LDIF_SUCCESSFUL_DETAILS=The import finished \
diff --git a/opends/src/server/org/opends/server/tools/dsreplication/ReplicationCliMain.java b/opends/src/server/org/opends/server/tools/dsreplication/ReplicationCliMain.java
index e603575..16f8b62 100644
--- a/opends/src/server/org/opends/server/tools/dsreplication/ReplicationCliMain.java
+++ b/opends/src/server/org/opends/server/tools/dsreplication/ReplicationCliMain.java
@@ -335,8 +335,7 @@
     // program.
     try
     {
-      argParser = new ReplicationCliArgumentParser(CLASS_NAME);
-      argParser.initializeParser(getOutputStream());
+      createArgumenParser();
     }
     catch (ArgumentException ae)
     {
@@ -574,6 +573,12 @@
     return returnValue.getReturnCode();
   }
 
+  private void createArgumenParser() throws ArgumentException
+  {
+    argParser = new ReplicationCliArgumentParser(CLASS_NAME);
+    argParser.initializeParser(getOutputStream());
+  }
+
   /**
    * Based on the data provided in the command-line it enables replication
    * between two servers.
@@ -7588,9 +7593,29 @@
     }
   }
 
-  private void initializeAllSuffix(String baseDN, InitialLdapContext ctx,
+  /**
+   * Initializes all the replicas in the topology with the contents of a
+   * given replica.
+   * @param ctx the connection to the server where the source replica of the
+   * initialization is.
+   * @param baseDN the dn of the suffix.
+   * @param displayProgress whether we want to display progress or not.
+   * @throws ReplicationCliException if an unexpected error occurs.
+   */
+  public void initializeAllSuffix(String baseDN, InitialLdapContext ctx,
   boolean displayProgress) throws ReplicationCliException
   {
+    if (argParser == null)
+    {
+      try
+      {
+        createArgumenParser();
+      }
+      catch (ArgumentException ae)
+      {
+        throw new RuntimeException("Error creating argument parser: "+ae, ae);
+      }
+    }
     int nTries = 5;
     boolean initDone = false;
     while (!initDone)
@@ -7836,9 +7861,10 @@
   }
 
   /**
-   * Initializes a suffix with the contents of a replica that has a given
-   * replication id.
-   * @param ctx the connection to the server whose suffix we want to initialize.
+   * Initializes all the replicas in the topology with the contents of a
+   * given replica.  This method will try to create the task only once.
+   * @param ctx the connection to the server where the source replica of the
+   * initialization is.
    * @param baseDN the dn of the suffix.
    * @param displayProgress whether we want to display progress or not.
    * @throws ApplicationException if an unexpected error occurs.

--
Gitblit v1.10.0