Add search fields for # of changed lines.

Based off https://gerrit-review.googlesource.com/#/c/52190, but
implementing the final suggestion of indexing raw delta counts and
allowing arbitrary range queries off of those.

Also upgrade Lucene to 4.8.1 as this was released since the last
schema change (which was on 4.7.0).

Change-Id: Ia8a677e71e133f68eced4c5394df1d23efe7f12a
This commit is contained in:
Jeff Davidson 2014-05-21 18:48:33 -07:00
parent 063f658042
commit 45d0a772e1
19 changed files with 478 additions and 45 deletions

View File

@ -314,6 +314,18 @@ status:abandoned::
+
Change has been abandoned.
[[size]]
added:'RELATION''LINES', deleted:'RELATION''LINES', delta/size:'RELATION''LINES'::
+
True if the number of lines added/deleted/changed satisfies the given relation
for the given number of lines.
+
For example, added:>50 will be true for any change which adds at least 50
lines.
+
Valid relations are >=, >, <=, <, or no relation, which will match if the
number of lines is exactly equal.
== Argument Quoting

View File

@ -119,6 +119,11 @@ public class SearchSuggestOracle extends HighlightSuggestOracle {
suggestions.add("status:merged");
suggestions.add("status:abandoned");
suggestions.add("added:");
suggestions.add("deleted:");
suggestions.add("delta:");
suggestions.add("size:");
suggestions.add("AND");
suggestions.add("OR");
suggestions.add("NOT");

View File

@ -127,6 +127,8 @@ public class LuceneChangeIndex implements ChangeIndex {
Version lucene44 = Version.LUCENE_44;
@SuppressWarnings("deprecation")
Version lucene46 = Version.LUCENE_46;
@SuppressWarnings("deprecation")
Version lucene47 = Version.LUCENE_47;
for (Map.Entry<Integer, Schema<ChangeData>> e
: ChangeSchemas.ALL.entrySet()) {
if (e.getKey() <= 3) {
@ -135,8 +137,10 @@ public class LuceneChangeIndex implements ChangeIndex {
versions.put(e.getValue(), lucene44);
} else if (e.getKey() <= 8) {
versions.put(e.getValue(), lucene46);
} else if (e.getKey() <= 10) {
versions.put(e.getValue(), lucene47);
} else {
versions.put(e.getValue(), Version.LUCENE_47);
versions.put(e.getValue(), Version.LUCENE_48);
}
}
LUCENE_VERSIONS = versions.build();
@ -497,7 +501,7 @@ public class LuceneChangeIndex implements ChangeIndex {
FieldType<?> type = values.getField().getType();
Store store = store(values.getField());
if (type == FieldType.INTEGER) {
if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
for (Object value : values.getValues()) {
doc.add(new IntField(name, (Integer) value, store));
}

View File

@ -22,6 +22,7 @@ import com.google.common.collect.Lists;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.index.IntegerRangePredicate;
import com.google.gerrit.server.index.RegexPredicate;
import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.index.TimestampRangePredicate;
@ -135,6 +136,8 @@ public class QueryBuilder {
throws QueryParseException {
if (p.getType() == FieldType.INTEGER) {
return intQuery(p);
} else if (p.getType() == FieldType.INTEGER_RANGE) {
return intRangeQuery(p);
} else if (p.getType() == FieldType.TIMESTAMP) {
return timestampQuery(p);
} else if (p.getType() == FieldType.EXACT) {
@ -169,6 +172,28 @@ public class QueryBuilder {
return new TermQuery(intTerm(p.getField().getName(), value));
}
private Query intRangeQuery(IndexPredicate<ChangeData> p)
throws QueryParseException {
if (p instanceof IntegerRangePredicate) {
IntegerRangePredicate<ChangeData> r =
(IntegerRangePredicate<ChangeData>) p;
int minimum = r.getMinimumValue();
int maximum = r.getMaximumValue();
if (minimum == maximum) {
// Just fall back to a standard integer query.
return new TermQuery(intTerm(p.getField().getName(), minimum));
} else {
return NumericRangeQuery.newIntRange(
r.getField().getName(),
minimum,
maximum,
true,
true);
}
}
throw new QueryParseException("not an integer range: " + p);
}
private Query sortKeyQuery(SortKeyPredicate p) {
long min = p.getMinValue(schema);
long max = p.getMaxValue(schema);

View File

@ -29,6 +29,7 @@ import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
import com.google.gwtorm.protobuf.CodecFactory;
import com.google.gwtorm.protobuf.ProtobufCodec;
import com.google.gwtorm.server.OrmException;
@ -428,6 +429,40 @@ public class ChangeField {
}
};
/** The number of inserted lines in this change. */
public static final FieldDef<ChangeData, Integer> ADDED =
new FieldDef.Single<ChangeData, Integer>(
ChangeQueryBuilder.FIELD_ADDED, FieldType.INTEGER_RANGE, true) {
@Override
public Integer get(ChangeData input, FillArgs args)
throws OrmException {
return input.changedLines().insertions;
}
};
/** The number of deleted lines in this change. */
public static final FieldDef<ChangeData, Integer> DELETED =
new FieldDef.Single<ChangeData, Integer>(
ChangeQueryBuilder.FIELD_DELETED, FieldType.INTEGER_RANGE, true) {
@Override
public Integer get(ChangeData input, FillArgs args)
throws OrmException {
return input.changedLines().deletions;
}
};
/** The total number of modified lines in this change. */
public static final FieldDef<ChangeData, Integer> DELTA =
new FieldDef.Single<ChangeData, Integer>(
ChangeQueryBuilder.FIELD_DELTA, FieldType.INTEGER_RANGE, false) {
@Override
public Integer get(ChangeData input, FillArgs args)
throws OrmException {
ChangedLines changedLines = input.changedLines();
return changedLines.insertions + changedLines.deletions;
}
};
private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
throws OrmException {
List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());

View File

@ -217,6 +217,32 @@ public class ChangeSchemas {
ChangeField.APPROVAL,
ChangeField.MERGEABLE);
static final Schema<ChangeData> V11 = release(
ChangeField.LEGACY_ID,
ChangeField.ID,
ChangeField.STATUS,
ChangeField.PROJECT,
ChangeField.PROJECTS,
ChangeField.REF,
ChangeField.TOPIC,
ChangeField.UPDATED,
ChangeField.FILE_PART,
ChangeField.PATH,
ChangeField.OWNER,
ChangeField.REVIEWER,
ChangeField.COMMIT,
ChangeField.TR,
ChangeField.LABEL,
ChangeField.REVIEWED,
ChangeField.COMMIT_MESSAGE,
ChangeField.COMMENT,
ChangeField.CHANGE,
ChangeField.APPROVAL,
ChangeField.MERGEABLE,
ChangeField.ADDED,
ChangeField.DELETED,
ChangeField.DELTA);
private static Schema<ChangeData> release(Collection<FieldDef<ChangeData, ?>> fields) {

View File

@ -14,6 +14,7 @@
package com.google.gerrit.server.index;
import com.google.common.base.Preconditions;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gwtorm.server.OrmException;
@ -47,6 +48,8 @@ public abstract class FieldDef<I, T> {
extends FieldDef<I, Iterable<T>> {
Repeatable(String name, FieldType<T> type, boolean stored) {
super(name, type, stored);
Preconditions.checkArgument(type != FieldType.INTEGER_RANGE,
"Range queries against repeated fields are unsupported");
}
@Override

View File

@ -23,6 +23,10 @@ public class FieldType<T> {
public static final FieldType<Integer> INTEGER =
new FieldType<Integer>("INTEGER");
/** A single-integer-valued field matched using range queries. */
public static final FieldType<Integer> INTEGER_RANGE =
new FieldType<Integer>("INTEGER_RANGE");
/** A single integer-valued field. */
public static final FieldType<Long> LONG =
new FieldType<Long>("LONG");

View File

@ -0,0 +1,56 @@
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.index;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.util.RangeUtil;
import com.google.gerrit.server.util.RangeUtil.Range;
import com.google.gwtorm.server.OrmException;
public abstract class IntegerRangePredicate<T> extends IndexPredicate<T> {
private final Range range;
protected IntegerRangePredicate(FieldDef<T, Integer> type,
String value) throws QueryParseException {
super(type, value);
range = RangeUtil.getRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE);
if (range == null) {
throw new QueryParseException("Invalid range predicate: " + value);
}
}
protected abstract int getValueInt(T object) throws OrmException;
@Override
public boolean match(T object) throws OrmException {
int valueInt = getValueInt(object);
return valueInt >= range.min && valueInt <= range.max;
}
/** Return the minimum value of this predicate's range, inclusive. */
public int getMinimumValue() {
return range.min;
}
/** Return the maximum value of this predicate's range, inclusive. */
public int getMaximumValue() {
return range.max;
}
@Override
public int getCost() {
return 1;
}
}

View File

@ -25,6 +25,7 @@ import com.google.gwtjsonrpc.common.JavaSqlTimestampHelper;
import java.sql.Timestamp;
import java.util.Date;
// TODO: Migrate this to IntegerRangePredicate
public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
@SuppressWarnings({"deprecation", "unchecked"})
protected static FieldDef<ChangeData, Timestamp> updatedField(

View File

@ -0,0 +1,31 @@
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.query.change;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.IntegerRangePredicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
public class AddedPredicate extends IntegerRangePredicate<ChangeData> {
AddedPredicate(String value) throws QueryParseException {
super(ChangeField.ADDED, value);
}
@Override
protected int getValueInt(ChangeData changeData) throws OrmException {
return changeData.changedLines().insertions;
}
}

View File

@ -75,6 +75,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
// NOTE: As new search operations are added, please keep the
// SearchSuggestOracle up to date.
public static final String FIELD_ADDED = "added";
public static final String FIELD_AFTER = "after";
public static final String FIELD_AGE = "age";
public static final String FIELD_BEFORE = "before";
@ -83,6 +84,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
public static final String FIELD_COMMENT = "comment";
public static final String FIELD_COMMIT = "commit";
public static final String FIELD_CONFLICTS = "conflicts";
public static final String FIELD_DELETED = "deleted";
public static final String FIELD_DELTA = "delta";
public static final String FIELD_DRAFTBY = "draftby";
public static final String FIELD_FILE = "file";
public static final String FIELD_IS = "is";
@ -676,6 +679,30 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return sortkey_before(sortKey);
}
@Operator
public Predicate<ChangeData> added(String value)
throws QueryParseException {
return new AddedPredicate(value);
}
@Operator
public Predicate<ChangeData> deleted(String value)
throws QueryParseException {
return new DeletedPredicate(value);
}
@Operator
public Predicate<ChangeData> size(String value)
throws QueryParseException {
return delta(value);
}
@Operator
public Predicate<ChangeData> delta(String value)
throws QueryParseException {
return new DeltaPredicate(value);
}
@Override
protected Predicate<ChangeData> defaultField(String query) {
if (query.startsWith("refs/")) {

View File

@ -0,0 +1,31 @@
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.query.change;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.IntegerRangePredicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
public class DeletedPredicate extends IntegerRangePredicate<ChangeData> {
DeletedPredicate(String value) throws QueryParseException {
super(ChangeField.DELETED, value);
}
@Override
protected int getValueInt(ChangeData changeData) throws OrmException {
return changeData.changedLines().deletions;
}
}

View File

@ -0,0 +1,33 @@
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.query.change;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.IntegerRangePredicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
import com.google.gwtorm.server.OrmException;
public class DeltaPredicate extends IntegerRangePredicate<ChangeData> {
DeltaPredicate(String value) throws QueryParseException {
super(ChangeField.DELTA, value);
}
@Override
protected int getValueInt(ChangeData changeData) throws OrmException {
ChangedLines changedLines = changeData.changedLines();
return changedLines.insertions + changedLines.deletions;
}
}

View File

@ -24,12 +24,12 @@ import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.OrPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.RangeUtil;
import com.google.gerrit.server.util.RangeUtil.Range;
import com.google.inject.Provider;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LabelPredicate extends OrPredicate<ChangeData> {
private static final int MAX_LABEL_VALUE = 4;
@ -102,43 +102,28 @@ public class LabelPredicate extends OrPredicate<ChangeData> {
// Try next format.
}
Range range;
if (parsed == null) {
Matcher m = Pattern.compile("(>|>=|=|<|<=)([+-]?\\d+)$").matcher(v);
if (m.find()) {
parsed = new Parsed(v.substring(0, m.start()), m.group(1),
value(m.group(2)));
} else {
parsed = new Parsed(v, "=", 1);
range = RangeUtil.getRange(v, -MAX_LABEL_VALUE, MAX_LABEL_VALUE);
if (range == null) {
range = new Range(v, 1, 1);
}
} else {
range = RangeUtil.getRange(
parsed.label,
parsed.test,
parsed.expVal,
-MAX_LABEL_VALUE,
MAX_LABEL_VALUE);
}
String prefix = range.prefix;
int min = range.min;
int max = range.max;
int min, max;
switch (parsed.test) {
case "=":
default:
min = max = parsed.expVal;
break;
case ">":
min = parsed.expVal + 1;
max = MAX_LABEL_VALUE;
break;
case ">=":
min = parsed.expVal;
max = MAX_LABEL_VALUE;
break;
case "<":
min = -MAX_LABEL_VALUE;
max = parsed.expVal - 1;
break;
case "<=":
min = -MAX_LABEL_VALUE;
max = parsed.expVal;
break;
}
List<Predicate<ChangeData>> r =
Lists.newArrayListWithCapacity(max - min + 1);
for (int i = min; i <= max; i++) {
r.add(onePredicate(args, parsed.label, i));
r.add(onePredicate(args, prefix, i));
}
return r;
}
@ -152,13 +137,6 @@ public class LabelPredicate extends OrPredicate<ChangeData> {
}
}
private static int value(String value) {
if (value.startsWith("+")) {
value = value.substring(1);
}
return Integer.parseInt(value);
}
private static Predicate<ChangeData> noLabelQuery(Args args, String label) {
List<Predicate<ChangeData>> r =
Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);

View File

@ -0,0 +1,119 @@
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.util;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class RangeUtil {
private static final Pattern RANGE_PATTERN =
Pattern.compile("(>|>=|=|<|<=|)([+-]?\\d+)$");
private RangeUtil() {}
public static class Range {
/** The prefix of the query, before the range component. */
public final String prefix;
/** The minimum value specified in the query, inclusive. */
public final int min;
/** The maximum value specified in the query, inclusive. */
public final int max;
public Range(String prefix, int min, int max) {
this.prefix = prefix;
this.min = min;
this.max = max;
}
}
/**
* Determine the range of values being requested in the given query.
*
* @param rangeQuery the raw query, e.g. "added:>12345"
* @param minValue the minimum possible value for the field, inclusive
* @param maxValue the maximum possible value for the field, inclusive
* @return the calculated {@link Range}, or null if the query is invalid
*/
@Nullable
public static Range getRange(String rangeQuery, int minValue, int maxValue) {
Matcher m = RANGE_PATTERN.matcher(rangeQuery);
String prefix;
String test;
Integer queryInt;
if (m.find()) {
prefix = rangeQuery.substring(0, m.start());
test = m.group(1);
queryInt = value(m.group(2));
if (queryInt == null) {
return null;
}
} else {
return null;
}
return getRange(prefix, test, queryInt, minValue, maxValue);
}
/**
* Determine the range of values being requested in the given query.
*
* @param prefix a prefix string which is copied into the range
* @param test the test operator, one of &gt;, &gt;=, =, &lt;, or &lt;=
* @param queryInt the integer being queried
* @param minValue the minimum possible value for the field, inclusive
* @param maxValue the maximum possible value for the field, inclusive
* @return the calculated {@link Range}
*/
public static Range getRange(
String prefix, String test, int queryInt, int minValue, int maxValue) {
int min, max;
switch (test) {
case "=":
default:
min = max = queryInt;
break;
case ">":
min = Ints.saturatedCast(queryInt + 1L);
max = maxValue;
break;
case ">=":
min = queryInt;
max = maxValue;
break;
case "<":
min = minValue;
max = Ints.saturatedCast(queryInt - 1L);
break;
case "<=":
min = minValue;
max = queryInt;
break;
}
return new Range(prefix, min, max);
}
private static Integer value(String value) {
if (value.startsWith("+")) {
value = value.substring(1);
}
return Ints.tryParse(value);
}
}

View File

@ -813,6 +813,44 @@ public abstract class AbstractQueryChangesTest {
assertResultEquals(change1, results.get(1));
}
@Test
public void bySize() throws Exception {
TestRepository<InMemoryRepository> repo = createProject("repo");
// added = 3, deleted = 0, delta = 3
RevCommit commit1 = repo.parseBody(
repo.commit().add("file1", "foo\n\foo\nfoo").create());
// added = 0, deleted = 2, delta = 2
RevCommit commit2 = repo.parseBody(
repo.commit().parent(commit1).add("file1", "foo").create());
Change change1 = newChange(repo, commit1, null, null, null).insert();
Change change2 = newChange(repo, commit2, null, null, null).insert();
assertTrue(query("added:>4").isEmpty());
assertResultEquals(change1, queryOne("added:3"));
assertResultEquals(change1, queryOne("added:>2"));
assertResultEquals(change1, queryOne("added:>=3"));
assertResultEquals(change2, queryOne("added:<1"));
assertResultEquals(change2, queryOne("added:<=0"));
assertTrue(query("deleted:>3").isEmpty());
assertResultEquals(change2, queryOne("deleted:2"));
assertResultEquals(change2, queryOne("deleted:>1"));
assertResultEquals(change2, queryOne("deleted:>=2"));
assertResultEquals(change1, queryOne("deleted:<1"));
assertResultEquals(change1, queryOne("deleted:<=0"));
for (String str : Lists.newArrayList("delta", "size")) {
assertTrue(query(str + ":<2").isEmpty());
assertResultEquals(change1, queryOne(str + ":3"));
assertResultEquals(change1, queryOne(str + ":>2"));
assertResultEquals(change1, queryOne(str + ":>=3"));
assertResultEquals(change2, queryOne(str + ":<3"));
assertResultEquals(change2, queryOne(str + ":<=2"));
}
}
@Test
public void byDefault() throws Exception {
TestRepository<InMemoryRepository> repo = createProject("repo");

View File

@ -54,6 +54,11 @@ public class LuceneQueryChangesV7Test extends AbstractQueryChangesTest {
@Override
@Test
public void byDefault() {}
@Ignore
@Override
@Test
public void bySize() {}
// End tests for features not supported in V7.
@Test

View File

@ -1,11 +1,11 @@
include_defs('//lib/maven.defs')
VERSION = '4.7.0'
VERSION = '4.8.1'
maven_jar(
name = 'core',
id = 'org.apache.lucene:lucene-core:' + VERSION,
sha1 = '12d2b92d15158ac0d7b2864f537403acb4d7f69e',
sha1 = 'a549eef6316a2c38d4cda932be809107deeaf8a7',
license = 'Apache2.0',
exclude = [
'META-INF/LICENSE.txt',
@ -16,7 +16,7 @@ maven_jar(
maven_jar(
name = 'analyzers-common',
id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
sha1 = '399fa6b0d750c8e5c9e4ae73e6407c8b3ed4e8c1',
sha1 = '6e3731524351c83cd21022a23bee5e87f0575555',
license = 'Apache2.0',
exclude = [
'META-INF/LICENSE.txt',
@ -27,6 +27,6 @@ maven_jar(
maven_jar(
name = 'query-parser',
id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
sha1 = 'f78a804de1582c511224d214c2d9c82ce48379e7',
sha1 = 'f3e105d74137906fdeb2c7bc4dd68c08564778f9',
license = 'Apache2.0',
)