From 943c10cc9d2d60660c53271a25848ecad77a91e7 Mon Sep 17 00:00:00 2001
From: pgamba <pgamba@localhost>
Date: Thu, 15 Oct 2009 13:15:17 +0000
Subject: [PATCH] Fix #4263 ECL entries should include creatorsName and createTimestamp
---
opends/resource/schema/03-changelog.ldif | 6 +
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ExternalChangeLogTest.java | 94 +++++++++++----
opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java | 180 +++++++++++++++++++++++------
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java | 5
opends/src/server/org/opends/server/replication/protocol/DeleteMsg.java | 47 +++++++
opends/src/server/org/opends/server/replication/plugin/LDAPReplicationDomain.java | 14 ++
6 files changed, 279 insertions(+), 67 deletions(-)
diff --git a/opends/resource/schema/03-changelog.ldif b/opends/resource/schema/03-changelog.ldif
index ae49c99..cab0b1f 100644
--- a/opends/resource/schema/03-changelog.ldif
+++ b/opends/resource/schema/03-changelog.ldif
@@ -89,6 +89,12 @@
SINGLE-VALUE
USAGE directoryOperation
X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.604 NAME 'changeInitiatorsName'
+ DESC 'The initiator user of the change'
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ SINGLE-VALUE
+ USAGE directoryOperation
+ X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.1 NAME 'changeLogEntry' SUP top
STRUCTURAL
MUST ( changeNumber $ targetDN $ changeType $ changeTime )
diff --git a/opends/src/server/org/opends/server/replication/plugin/LDAPReplicationDomain.java b/opends/src/server/org/opends/server/replication/plugin/LDAPReplicationDomain.java
index 898b387..78178b7 100644
--- a/opends/src/server/org/opends/server/replication/plugin/LDAPReplicationDomain.java
+++ b/opends/src/server/org/opends/server/replication/plugin/LDAPReplicationDomain.java
@@ -4537,6 +4537,20 @@
newattrs.add(a);
}
((DeleteMsg)msg).setEclIncludes(newattrs);
+
+ // For delete only, add the modifiersName since they are required in the
+ // ECL entry but are not part of other parts of the message
+ AttributeType atype = DirectoryServer.getAttributeType("modifiersname");
+ List<Attribute> attrs = entry.getAttribute(atype);
+ if (attrs != null)
+ {
+ for (Attribute a : attrs)
+ {
+ ((DeleteMsg)msg).setInitiatorsName(a.iterator().next().toString());
+ break;
+ }
+ }
+
}
else if (op instanceof PostOperationModifyOperation)
{
diff --git a/opends/src/server/org/opends/server/replication/protocol/DeleteMsg.java b/opends/src/server/org/opends/server/replication/protocol/DeleteMsg.java
index b3d685e..4068db1 100644
--- a/opends/src/server/org/opends/server/replication/protocol/DeleteMsg.java
+++ b/opends/src/server/org/opends/server/replication/protocol/DeleteMsg.java
@@ -43,6 +43,8 @@
*/
public class DeleteMsg extends LDAPUpdateMsg
{
+ String initiatorsName;
+
/**
* Creates a new delete message.
*
@@ -137,13 +139,28 @@
byte[] byteEntryAttrLen =
String.valueOf(encodedEclIncludes.length).getBytes("UTF-8");
+
bodyLength += byteEntryAttrLen.length + 1;
bodyLength += encodedEclIncludes.length + 1;
+ byte[] byteInitiatorsName = null;
+ if (initiatorsName != null)
+ {
+ byteInitiatorsName = initiatorsName.getBytes("UTF-8");
+ bodyLength += byteInitiatorsName.length + 1;
+ }
+ else
+ {
+ bodyLength++;
+ }
/* encode the header in a byte[] large enough to also contain the mods */
byte [] encodedMsg = encodeHeader(MSG_TYPE_DELETE, bodyLength,
ProtocolVersion.REPLICATION_PROTOCOL_V4);
int pos = encodedMsg.length - bodyLength;
+ if (byteInitiatorsName != null)
+ pos = addByteArray(byteInitiatorsName, encodedMsg, pos);
+ else
+ encodedMsg[pos++] = 0;
pos = addByteArray(byteEntryAttrLen, encodedMsg, pos);
pos = addByteArray(encodedEclIncludes, encodedMsg, pos);
return encodedMsg;
@@ -158,6 +175,17 @@
{
// Read ecl attr len
int length = getNextLength(in, pos);
+ if (length != 0)
+ {
+ initiatorsName = new String(in, pos, length, "UTF-8");
+ pos += length + 1;
+ }
+ else
+ {
+ initiatorsName = null;
+ pos += 1;
+ }
+ length = getNextLength(in, pos);
int eclAttrLen = Integer.valueOf(new String(in, pos, length,"UTF-8"));
pos += length + 1;
@@ -216,4 +244,23 @@
return encodedEclIncludes.length + headerSize();
}
+ /**
+ * Set the initiator's name of this change.
+ *
+ * @param iname the initiator's name.
+ */
+ public void setInitiatorsName(String iname)
+ {
+ initiatorsName = iname;
+ }
+
+ /**
+ * Get the initiator's name of this change.
+ * @return the initiator's name.
+ */
+ public String getInitiatorsName()
+ {
+ return initiatorsName;
+ }
+
}
diff --git a/opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java b/opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java
index 150fb68..c49158c 100644
--- a/opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java
+++ b/opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java
@@ -62,6 +62,7 @@
import org.opends.server.core.AccessControlConfigManager;
import org.opends.server.core.AddOperation;
import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyDNOperation;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.PersistentSearch;
import org.opends.server.core.PluginConfigManager;
@@ -733,7 +734,7 @@
null, // real time current entry
eclAttributes, // entry attributes
eclmsg.getDraftChangeNumber(),
- "add");
+ "add", null);
} else
if (msg instanceof ModifyMsg)
@@ -761,7 +762,7 @@
null, // real time current entry
eclAttributes, // entry attributes
eclmsg.getDraftChangeNumber(),
- "modify");
+ "modify",null);
}
catch(Exception e)
@@ -774,35 +775,50 @@
}
else if (msg instanceof ModifyDNMsg)
{
- ModifyDNMsg modDNMsg = (ModifyDNMsg)msg;
-
- ArrayList<RawAttribute> eclAttributes = modDNMsg.getEclIncludes();
-
- clEntry = createChangelogEntry(
- eclmsg.getServiceId(),
- eclmsg.getCookie().toString(),
- DN.decode(modDNMsg.getDn()),
- modDNMsg.getChangeNumber(),
- null,
- modDNMsg.getUniqueId(),
- null, // real time current entry
- eclAttributes, // entry attributes
- eclmsg.getDraftChangeNumber(),
- "modrdn");
-
- Attribute a = Attributes.create("newrdn", modDNMsg.getNewRDN());
- clEntry.addAttribute(a, null);
-
- if (modDNMsg.getNewSuperior()!=null)
+ try
{
- Attribute b = Attributes.create("newsuperior",
- modDNMsg.getNewSuperior());
- clEntry.addAttribute(b, null);
- }
+ InternalClientConnection conn =
+ InternalClientConnection.getRootConnection();
+ ModifyDNMsg modDNMsg = (ModifyDNMsg)msg;
- Attribute c = Attributes.create("deleteoldrdn",
- String.valueOf(modDNMsg.deleteOldRdn()));
- clEntry.addAttribute(c, null);
+ ArrayList<RawAttribute> eclAttributes = modDNMsg.getEclIncludes();
+ ModifyDNOperation modifyDNOperation =
+ (ModifyDNOperation)modDNMsg.createOperation(conn);
+ String LDIFchanges = modToLDIF(modifyDNOperation.getModifications());
+
+ clEntry = createChangelogEntry(
+ eclmsg.getServiceId(),
+ eclmsg.getCookie().toString(),
+ DN.decode(modDNMsg.getDn()),
+ modDNMsg.getChangeNumber(),
+ LDIFchanges,
+ modDNMsg.getUniqueId(),
+ null, // real time current entry
+ eclAttributes, // entry attributes
+ eclmsg.getDraftChangeNumber(),
+ "modrdn", null);
+
+ Attribute a = Attributes.create("newrdn", modDNMsg.getNewRDN());
+ clEntry.addAttribute(a, null);
+
+ if (modDNMsg.getNewSuperior()!=null)
+ {
+ Attribute b = Attributes.create("newsuperior",
+ modDNMsg.getNewSuperior());
+ clEntry.addAttribute(b, null);
+ }
+
+ Attribute c = Attributes.create("deleteoldrdn",
+ String.valueOf(modDNMsg.deleteOldRdn()));
+ clEntry.addAttribute(c, null);
+ }
+ catch(Exception e)
+ {
+ // Exceptions raised by createOperation for example
+ throw new DirectoryException(ResultCode.OTHER,
+ Message.raw(Category.SYNC, Severity.NOTICE,
+ " Server fails to create entry: "),e);
+ }
}
else if (msg instanceof DeleteMsg)
@@ -821,7 +837,7 @@
null,
eclAttributes, // entry attributes
eclmsg.getDraftChangeNumber(),
- "delete");
+ "delete", delMsg.getInitiatorsName());
}
return clEntry;
}
@@ -861,6 +877,7 @@
* @param histEntryAttributes TODO:ECL Adress hist entry attributes
* @param draftChangenumber The provided draft change number (integer)
* @param changetype The provided change type (add, ...)
+ * @param delInitiatorsName The provided del initiatiors name
* @return The created ECL entry.
* @throws DirectoryException
* When any error occurs.
@@ -875,12 +892,14 @@
Entry entry,
List<RawAttribute> histEntryAttributes,
int draftChangenumber,
- String changetype)
+ String changetype,
+ String delInitiatorsName)
throws DirectoryException
{
AttributeType attributeType;
String dnString = "";
+ String pattern;
if (draftChangenumber == 0)
{
// Draft uncompat mode
@@ -1007,13 +1026,94 @@
if (clearLDIFchanges != null)
{
- if((attributeType =
- DirectoryServer.getAttributeType("changes")) == null)
- attributeType =
+ if (changetype.equals("add"))
+ {
+ if((attributeType =
+ DirectoryServer.getAttributeType("changes")) == null)
+ attributeType =
DirectoryServer.getDefaultAttributeType("changes");
- a = Attributes.create(attributeType, clearLDIFchanges + "\n");
- // force base64
+ a = Attributes.create(attributeType, clearLDIFchanges + "\n");
+ // force base64
+ attrList = new ArrayList<Attribute>(1);
+ attrList.add(a);
+ if(attributeType.isOperational())
+ operationalAttrs.put(attributeType, attrList);
+ else
+ uAttrs.put(attributeType, attrList);
+
+ pattern = "creatorsName: ";
+ int att_cr = clearLDIFchanges.indexOf(pattern);
+ if (att_cr>0)
+ {
+ int start_val_cr = clearLDIFchanges.indexOf(':', att_cr);
+ int end_val_cr = clearLDIFchanges.indexOf(EOL, att_cr);
+ String creatorsName =
+ clearLDIFchanges.substring(start_val_cr+2, end_val_cr);
+
+ if((attributeType =
+ DirectoryServer.getAttributeType("changeInitiatorsName")) == null)
+ attributeType =
+ DirectoryServer.getDefaultAttributeType("changeInitiatorsName");
+ a = Attributes.create(attributeType, creatorsName);
+ attrList = new ArrayList<Attribute>(1);
+ attrList.add(a);
+ if(attributeType.isOperational())
+ operationalAttrs.put(attributeType, attrList);
+ else
+ uAttrs.put(attributeType, attrList);
+ }
+ }
+ else if (changetype.equals("modify")||changetype.equals("modrdn"))
+ {
+ if (changetype.equals("modify"))
+ {
+ if((attributeType =
+ DirectoryServer.getAttributeType("changes")) == null)
+ attributeType =
+ DirectoryServer.getDefaultAttributeType("changes");
+
+ a = Attributes.create(attributeType, clearLDIFchanges + "\n");
+ // force base64
+ attrList = new ArrayList<Attribute>(1);
+ attrList.add(a);
+ if(attributeType.isOperational())
+ operationalAttrs.put(attributeType, attrList);
+ else
+ uAttrs.put(attributeType, attrList);
+ }
+
+ pattern = "modifiersName: ";
+ int att_cr = clearLDIFchanges.indexOf(pattern);
+ if (att_cr>0)
+ {
+ int start_val_cr = att_cr + pattern.length();
+ int end_val_cr = clearLDIFchanges.indexOf(EOL, att_cr);
+ String modifiersName =
+ clearLDIFchanges.substring(start_val_cr, end_val_cr);
+
+ if((attributeType =
+ DirectoryServer.getAttributeType("changeInitiatorsName")) == null)
+ attributeType =
+ DirectoryServer.getDefaultAttributeType("changeInitiatorsName");
+ a = Attributes.create(attributeType, modifiersName);
+ attrList = new ArrayList<Attribute>(1);
+ attrList.add(a);
+ if(attributeType.isOperational())
+ operationalAttrs.put(attributeType, attrList);
+ else
+ uAttrs.put(attributeType, attrList);
+ }
+ }
+ }
+
+ if (changetype.equals("delete") && (delInitiatorsName!=null))
+ {
+ if((attributeType =
+ DirectoryServer.getAttributeType("changeInitiatorsName")) == null)
+ attributeType =
+ DirectoryServer.getDefaultAttributeType("changeInitiatorsName");
+ a = Attributes.create(attributeType, delInitiatorsName);
attrList = new ArrayList<Attribute>(1);
attrList.add(a);
if(attributeType.isOperational())
@@ -1072,7 +1172,8 @@
{
try
{
- String eclName = "target" + ra.getAttributeType().toLowerCase();
+ String attrName = ra.getAttributeType().toLowerCase();
+ String eclName = "target" + attrName;
AttributeBuilder builder = new AttributeBuilder(
DirectoryServer.getDefaultAttributeType(eclName));
AttributeType at = builder.getAttributeType();
@@ -1250,6 +1351,11 @@
*/
public static String modToLDIF(List<Modification> mods)
{
+ if (mods==null)
+ {
+ // test case only
+ return null;
+ }
StringBuilder modTypeLine = new StringBuilder();
Iterator<Modification> iterator = mods.iterator();
while (iterator.hasNext())
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ExternalChangeLogTest.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ExternalChangeLogTest.java
index 77f0fce..e646e29 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ExternalChangeLogTest.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ExternalChangeLogTest.java
@@ -27,6 +27,7 @@
package org.opends.server.replication;
import static org.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING;
+import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.replication.protocol.OperationContext.SYNCHROCONTEXT;
@@ -54,6 +55,9 @@
import java.util.SortedSet;
import java.util.TreeSet;
+import org.opends.messages.Category;
+import org.opends.messages.Message;
+import org.opends.messages.Severity;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.Backend;
import org.opends.server.api.ConnectionHandler;
@@ -226,6 +230,7 @@
// Write additional changes and read ECL from a provided draft change number
ts = ECLCompatWriteReadAllOps(5);replicationServer.clearDb();
+ // ECLIncludeAttributes();replicationServer.clearDb();
}
@Test(enabled=true, groups="slow", dependsOnMethods = { "ECLReplicationServerTest"})
@@ -2428,10 +2433,10 @@
{
if (debugEnabled())
{
-// logError(Message.raw(Category.SYNC, Severity.NOTICE,
-// "** TEST " + tn + " ** " + s));
TRACER.debugInfo("** TEST " + tn + " ** " + s);
}
+ // logError(Message.raw(Category.SYNC, Severity.NOTICE,
+ // "** TEST " + tn + " ** " + s));
}
/**
@@ -3434,14 +3439,25 @@
{
String tn = "ECLIncludeAttributes";
debugInfo(tn, "Starting test\n\n");
+ Backend backend2 = null;
+ Backend backend3 = null;
+ ReplicationBroker server01 = null;
+ DeleteOperationBasis delOp =null;
+ LDAPReplicationDomain domain2 = null;
+ LDAPReplicationDomain domain3 = null;
+ LDAPReplicationDomain domain21 = null;
+ SynchronizationProvider replicationPlugin2 = null;
+ SynchronizationProvider replicationPlugin3 = null;
+ DN baseDn2 = null;
+ DN baseDn3 = null;
try
{
// Initialize a second test backend o=test2, in addtion to o=test
// Configure replication on this backend
// Add the root entry in the backend
- Backend backend2 = initializeTestBackend(false,
+ backend2 = initializeTestBackend(false,
TEST_ROOT_DN_STRING2, TEST_BACKEND_ID2);
- DN baseDn2 = DN.decode(TEST_ROOT_DN_STRING2);
+ baseDn2 = DN.decode(TEST_ROOT_DN_STRING2);
SortedSet<String> replServers = new TreeSet<String>();
replServers.add("localhost:"+replicationServerPort);
DomainFakeCfg domainConf =
@@ -3449,20 +3465,20 @@
SortedSet<String> includeAttributes = new TreeSet<String>();
includeAttributes.add("sn");
domainConf.setEclIncludes(includeAttributes);
- LDAPReplicationDomain domain2 = MultimasterReplication.createNewDomain(domainConf);
- SynchronizationProvider replicationPlugin2 = new MultimasterReplication();
+ domain2 = MultimasterReplication.createNewDomain(domainConf);
+ replicationPlugin2 = new MultimasterReplication();
replicationPlugin2.completeSynchronizationProvider();
- Backend backend3 = initializeTestBackend(false,
+ backend3 = initializeTestBackend(false,
TEST_ROOT_DN_STRING3, TEST_BACKEND_ID3);
- DN baseDn3 = DN.decode(TEST_ROOT_DN_STRING3);
+ baseDn3 = DN.decode(TEST_ROOT_DN_STRING3);
domainConf =
new DomainFakeCfg(baseDn3, 1703, replServers);
includeAttributes = new TreeSet<String>();
includeAttributes.add("objectclass");
domainConf.setEclIncludes(includeAttributes);
- LDAPReplicationDomain domain3 = MultimasterReplication.createNewDomain(domainConf);
- SynchronizationProvider replicationPlugin3 = new MultimasterReplication();
+ domain3 = MultimasterReplication.createNewDomain(domainConf);
+ replicationPlugin3 = new MultimasterReplication();
replicationPlugin3.completeSynchronizationProvider();
domainConf =
@@ -3470,11 +3486,11 @@
includeAttributes = new TreeSet<String>();
includeAttributes.add("cn");
domainConf.setEclIncludes(includeAttributes);
- LDAPReplicationDomain domain21 = MultimasterReplication.createNewDomain(domainConf);
+ domain21 = MultimasterReplication.createNewDomain(domainConf);
Set<String> attrList = new HashSet<String>();
attrList.add(new String("cn"));
- ReplicationBroker server01 = openReplicationSession(
+ server01 = openReplicationSession(
DN.decode(TEST_ROOT_DN_STRING2), 1206,
100, replicationServerPort,
1000, true, -1 , domain21);
@@ -3550,7 +3566,7 @@
waitOpResult(modDNOp, ResultCode.SUCCESS);
//
- DeleteOperationBasis delOp = new DeleteOperationBasis(connection,
+ delOp = new DeleteOperationBasis(connection,
InternalClientConnection.nextOperationID(),
InternalClientConnection.nextMessageID(), null,
DN.decode("cn=Robert Hue2," + TEST_ROOT_DN_STRING3));
@@ -3611,24 +3627,10 @@
checkValue(resultEntry,"targetsn","jensen");
checkValue(resultEntry,"targetcn","Fiona Jensen");
}
+ checkValue(resultEntry,"changeinitiatorsname", "cn=Internal Client,cn=Root DNs,cn=config");
}
}
assertEquals(entries.size(),8, "Entries number returned by search" + s);
- server01.stop();
-
- // Cleaning
- if (domain2 != null)
- MultimasterReplication.deleteDomain(baseDn2);
- if (replicationPlugin2 != null)
- DirectoryServer.deregisterSynchronizationProvider(replicationPlugin2);
- removeTestBackend2(backend2);
-
- if (domain3 != null)
- MultimasterReplication.deleteDomain(baseDn3);
- if (replicationPlugin3 != null)
- DirectoryServer.deregisterSynchronizationProvider(replicationPlugin3);
- removeTestBackend2(backend3);
-
}
catch(Exception e)
{
@@ -3637,7 +3639,43 @@
}
finally
{
+ try
+ {
+ server01.stop();
+ //
+ delOp = new DeleteOperationBasis(connection,
+ InternalClientConnection.nextOperationID(),
+ InternalClientConnection.nextMessageID(), null,
+ DN.decode("cn=Fiona Jensen," + TEST_ROOT_DN_STRING2));
+ delOp.run();
+ waitOpResult(delOp, ResultCode.SUCCESS);
+ delOp = new DeleteOperationBasis(connection,
+ InternalClientConnection.nextOperationID(),
+ InternalClientConnection.nextMessageID(), null,
+ DN.decode(TEST_ROOT_DN_STRING2));
+ delOp.run();
+ waitOpResult(delOp, ResultCode.SUCCESS);
+ delOp = new DeleteOperationBasis(connection,
+ InternalClientConnection.nextOperationID(),
+ InternalClientConnection.nextMessageID(), null,
+ DN.decode(TEST_ROOT_DN_STRING3));
+ delOp.run();
+ waitOpResult(delOp, ResultCode.SUCCESS);
+ // Cleaning
+ if (domain2 != null)
+ MultimasterReplication.deleteDomain(baseDn2);
+ if (replicationPlugin2 != null)
+ DirectoryServer.deregisterSynchronizationProvider(replicationPlugin2);
+ removeTestBackend2(backend2);
+
+ if (domain3 != null)
+ MultimasterReplication.deleteDomain(baseDn3);
+ if (replicationPlugin3 != null)
+ DirectoryServer.deregisterSynchronizationProvider(replicationPlugin3);
+ removeTestBackend2(backend3);
+ }
+ catch(Exception e) {}
}
debugInfo(tn, "Ending test with success");
}
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java
index 308a7df..10f188b 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java
@@ -350,11 +350,12 @@
{
msg.setEclIncludes(entryAttrList);
}
-
+ msg.setInitiatorsName("johnny h");
DeleteMsg generatedMsg = (DeleteMsg) ReplicationMsg.generateMsg(
msg.getBytes(), ProtocolVersion.getCurrentVersion());
assertEquals(msg.toString(), generatedMsg.toString());
+ assertEquals(msg.getInitiatorsName(), generatedMsg.getInitiatorsName());
assertEquals(msg.getChangeNumber(), generatedMsg.getChangeNumber());
@@ -374,7 +375,7 @@
i++;
}
}
-
+
Operation generatedOperation = generatedMsg.createOperation(connection);
assertEquals(generatedOperation.getClass(), DeleteOperationBasis.class);
--
Gitblit v1.10.0