diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java new file mode 100644 index 000000000..96ace08dd --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022, Matthias Sohn and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; + +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.junit.Test; + +public class AbbrevConfigTest extends RepositoryTestCase { + + @Test + public void testDefault() throws Exception { + assertEquals(7, testCoreAbbrev(null)); + } + + @Test + public void testAuto() throws Exception { + assertEquals(7, testCoreAbbrev("auto")); + } + + @Test + public void testNo() throws Exception { + assertEquals(40, testCoreAbbrev("no")); + } + + @Test + public void testValidMin() throws Exception { + assertEquals(4, testCoreAbbrev("4")); + } + + @Test + public void testValid() throws Exception { + assertEquals(22, testCoreAbbrev("22")); + } + + @Test + public void testValidMax() throws Exception { + assertEquals(40, testCoreAbbrev("40")); + } + + @Test + public void testInvalid() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("foo")); + } + + @Test + public void testInvalid2() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("2k")); + } + + @Test + public void testInvalidNegative() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("-1000")); + } + + @Test + public void testInvalidBelowRange() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("3")); + } + + @Test + public void testInvalidBelowRange2() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("-1")); + } + + @Test + public void testInvalidAboveRange() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("41")); + } + + @Test + public void testInvalidAboveRange2() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("100000")); + } + + @Test + public void testToStringNo() + throws InvalidConfigurationException, IOException { + assertEquals("40", setCoreAbbrev("no").toString()); + } + + @Test + public void testToString() + throws InvalidConfigurationException, IOException { + assertEquals("7", setCoreAbbrev("auto").toString()); + } + + @Test + public void testToString12() + throws InvalidConfigurationException, IOException { + assertEquals("12", setCoreAbbrev("12").toString()); + } + + private int testCoreAbbrev(String value) + throws InvalidConfigurationException, IOException { + return setCoreAbbrev(value).get(); + } + + private AbbrevConfig setCoreAbbrev(String value) + throws IOException, InvalidConfigurationException { + FileBasedConfig config = db.getConfig(); + config.setString(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_ABBREV, value); + config.save(); + return AbbrevConfig.parseFromConfig(db); + } + +} diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 88a712e7b..e026e31dc 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -23,6 +23,22 @@ + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 6e0d8f562..e6f4e65e7 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -366,6 +366,7 @@ invalidAncestryLength=Invalid ancestry length invalidBooleanValue=Invalid boolean value: {0}.{1}={2} invalidChannel=Invalid channel {0} invalidCommitParentNumber=Invalid commit parent number +invalidCoreAbbrev=Invalid value {0} of option core.abbrev invalidDepth=Invalid depth: {0} invalidEncoding=Invalid encoding from git config i18n.commitEncoding: {0} invalidEncryption=Invalid encryption diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java index 2480e2ebd..805a88639 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java @@ -9,9 +9,9 @@ */ package org.eclipse.jgit.api; -import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import static org.eclipse.jgit.lib.Constants.R_REFS; import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.lib.TypedConfigGetter.UNSET_INT; import java.io.IOException; import java.text.MessageFormat; @@ -34,6 +34,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.fnmatch.FileNameMatcher; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.AbbrevConfig; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -92,7 +93,7 @@ public class DescribeCommand extends GitCommand { /** * The prefix length to use when abbreviating a commit hash. */ - private int abbrev = OBJECT_ID_ABBREV_STRING_LENGTH; + private int abbrev = UNSET_INT; /** * Constructor for DescribeCommand. @@ -216,12 +217,17 @@ public DescribeCommand setAlways(boolean always) { * * @param abbrev * minimum length of the abbreviated string. Must be in the range - * [2, {@value Constants#OBJECT_ID_STRING_LENGTH}]. + * [{@value AbbrevConfig#MIN_ABBREV}, + * {@value Constants#OBJECT_ID_STRING_LENGTH}]. * @return {@code this} * @since 6.1 */ public DescribeCommand setAbbrev(int abbrev) { - this.abbrev = abbrev; + if (abbrev == 0) { + this.abbrev = 0; + } else { + this.abbrev = AbbrevConfig.capAbbrev(abbrev); + } return this; } @@ -232,13 +238,7 @@ private String longDescription(Ref tag, int depth, ObjectId tip) } return String.format("%s-%d-g%s", formatRefName(tag.getName()), //$NON-NLS-1$ Integer.valueOf(depth), - w.getObjectReader().abbreviate(tip, getCappedAbbrev()).name()); - } - - private int getCappedAbbrev() { - int len = Math.max(abbrev, 4); - len = Math.min(len, Constants.OBJECT_ID_STRING_LENGTH); - return len; + w.getObjectReader().abbreviate(tip, abbrev).name()); } /** @@ -330,6 +330,9 @@ public String call() throws GitAPIException { if (target == null) { setTarget(Constants.HEAD); } + if (abbrev == UNSET_INT) { + abbrev = AbbrevConfig.parseFromConfig(repo).get(); + } Collection tagList = repo.getRefDatabase() .getRefsByPrefix(useAll ? R_REFS : R_TAGS); @@ -443,7 +446,8 @@ String describe(ObjectId tip) throws IOException { if (candidates.isEmpty()) { return always ? w.getObjectReader() - .abbreviate(target, getCappedAbbrev()) + .abbreviate(target, + AbbrevConfig.capAbbrev(abbrev)) .name() : null; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 9623346b0..16b3f372e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -394,6 +394,7 @@ public static JGitText get() { /***/ public String invalidBooleanValue; /***/ public String invalidChannel; /***/ public String invalidCommitParentNumber; + /***/ public String invalidCoreAbbrev; /***/ public String invalidDepth; /***/ public String invalidEncoding; /***/ public String invalidEncryption; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java index 5b6894da9..99da22239 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java @@ -165,6 +165,15 @@ public void flush() { } }; } + + @Override + public long getApproximateObjectCount() { + long count = 0; + for (DfsPackDescription p : packs) { + count += p.getObjectCount(); + } + return count; + } } private static class MemPack extends DfsPackDescription { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java index 7dedeb57a..094fdc155 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java @@ -263,4 +263,17 @@ private static class UnpackedObjectId extends ObjectIdOwnerMap.Entry { private AlternateHandle.Id getAlternateId() { return wrapped.getAlternateId(); } + + @Override + public long getApproximateObjectCount() { + long count = 0; + for (Pack p : getPacks()) { + try { + count += p.getObjectCount(); + } catch (IOException e) { + return -1; + } + } + return count; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java index 627facca0..06c8cad3a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java @@ -212,6 +212,20 @@ public Collection getPacks() { return packed.getPacks(); } + /** {@inheritDoc} */ + @Override + public long getApproximateObjectCount() { + long count = 0; + for (Pack p : getPacks()) { + try { + count += p.getIndex().getObjectCount(); + } catch (IOException e) { + return -1; + } + } + return count; + } + /** * {@inheritDoc} *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java new file mode 100644 index 000000000..9109cfd76 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2022, Matthias Sohn and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; +import static org.eclipse.jgit.lib.TypedConfigGetter.UNSET_INT; + +import java.text.MessageFormat; + +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.internal.JGitText; + +/** + * Git configuration option + * core.abbrev + * + * @since 6.1 + */ +public final class AbbrevConfig { + private static final String VALUE_NO = "no"; //$NON-NLS-1$ + + private static final String VALUE_AUTO = "auto"; //$NON-NLS-1$ + + /** + * The minimum value of abbrev + */ + public static final int MIN_ABBREV = 4; + + /** + * Cap configured core.abbrev to range between minimum of 4 and number of + * hex-digits of a full object id. + * + * @param len + * configured number of hex-digits to abbreviate object ids to + * @return core.abbrev capped to range between minimum of 4 and number of + * hex-digits of a full object id + */ + public static int capAbbrev(int len) { + return Math.min(Math.max(MIN_ABBREV, len), + Constants.OBJECT_ID_STRING_LENGTH); + } + + /** + * No abbreviation + */ + public final static AbbrevConfig NO = new AbbrevConfig( + Constants.OBJECT_ID_STRING_LENGTH); + + /** + * Parse string value of core.abbrev git option for a given repository + * + * @param repo + * repository + * @return the parsed AbbrevConfig + * @throws InvalidConfigurationException + * if value of core.abbrev is invalid + */ + public static AbbrevConfig parseFromConfig(Repository repo) + throws InvalidConfigurationException { + Config config = repo.getConfig(); + String value = config.getString(ConfigConstants.CONFIG_CORE_SECTION, + null, ConfigConstants.CONFIG_KEY_ABBREV); + if (value == null || value.equalsIgnoreCase(VALUE_AUTO)) { + return auto(repo); + } + if (value.equalsIgnoreCase(VALUE_NO)) { + return NO; + } + try { + int len = config.getIntInRange(ConfigConstants.CONFIG_CORE_SECTION, + ConfigConstants.CONFIG_KEY_ABBREV, MIN_ABBREV, + Constants.OBJECT_ID_STRING_LENGTH, UNSET_INT); + if (len == UNSET_INT) { + // Unset was checked above. If we get UNSET_INT here, then + // either the value was UNSET_INT, or it was an invalid value + // (not an integer, or out of range), and EGit's + // ReportingTypedGetter caught the exception and has logged a + // warning. In either case we should fall back to some sane + // default. + len = OBJECT_ID_ABBREV_STRING_LENGTH; + } + return new AbbrevConfig(len); + } catch (IllegalArgumentException e) { + throw new InvalidConfigurationException(MessageFormat + .format(JGitText.get().invalidCoreAbbrev, value), e); + } + } + + /** + * An appropriate value is computed based on the approximate number of + * packed objects in a repository, which hopefully is enough for abbreviated + * object names to stay unique for some time. + * + * @param repo + * @return appropriate value computed based on the approximate number of + * packed objects in a repository + */ + private static AbbrevConfig auto(Repository repo) { + long count = repo.getObjectDatabase().getApproximateObjectCount(); + if (count == -1) { + return new AbbrevConfig(OBJECT_ID_ABBREV_STRING_LENGTH); + } + // find msb, round to next power of 2 + int len = 63 - Long.numberOfLeadingZeros(count) + 1; + // With the order of 2^len objects, we expect a collision at + // 2^(len/2). But we also care about hex chars, not bits, and + // there are 4 bits per hex. So all together we need to divide + // by 2; but we also want to round odd numbers up, hence adding + // one before dividing. + len = (len + 1) / 2; + // for small repos use at least fallback length + return new AbbrevConfig(Math.max(len, OBJECT_ID_ABBREV_STRING_LENGTH)); + } + + /** + * All other possible abbreviation lengths. Valid range 4 to number of + * hex-digits of an unabbreviated object id (40 for SHA1 object ids, jgit + * doesn't support SHA256 yet). + */ + private int abbrev; + + /** + * @param abbrev + */ + private AbbrevConfig(int abbrev) { + this.abbrev = capAbbrev(abbrev); + } + + /** + * Get the configured abbreviation length for object ids. + * + * @return the configured abbreviation length for object ids + */ + public int get() { + return abbrev; + } + + @Override + public String toString() { + return Integer.toString(abbrev); + } +} \ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java index 1ce3e312e..d1d66d280 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -277,6 +277,54 @@ public int getInt(final String section, String subsection, defaultValue); } + /** + * Obtain an integer value from the configuration which must be inside given + * range. + * + * @param section + * section the key is grouped within. + * @param name + * name of the key to get. + * @param minValue + * minimum value + * @param maxValue + * maximum value + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + * @since 6.1 + */ + public int getIntInRange(String section, String name, int minValue, + int maxValue, int defaultValue) { + return typedGetter.getIntInRange(this, section, null, name, minValue, + maxValue, defaultValue); + } + + /** + * Obtain an integer value from the configuration which must be inside given + * range. + * + * @param section + * section the key is grouped within. + * @param subsection + * subsection name, such a remote or branch name. + * @param name + * name of the key to get. + * @param minValue + * minimum value + * @param maxValue + * maximum value + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + * @since 6.1 + */ + public int getIntInRange(String section, String subsection, String name, + int minValue, int maxValue, int defaultValue) { + return typedGetter.getIntInRange(this, section, subsection, name, + minValue, maxValue, defaultValue); + } + /** * Obtain an integer value from the configuration. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 42d8aa544..80d720ae4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -836,4 +836,11 @@ public final class ConfigConstants { */ public static final String CONFIG_KEY_DEFAULT = "default"; + /** + * The "abbrev" key + * + * @since 6.1 + */ + public static final String CONFIG_KEY_ABBREV = "abbrev"; + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java index 04262c07a..70009cba3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java @@ -155,4 +155,14 @@ public ObjectLoader open(AnyObjectId objectId, int typeHint) public ObjectDatabase newCachedDatabase() { return this; } + + /** + * Get a quick, rough count of objects in this repository. Ignores loose + * objects. Returns {@code -1} if an exception occurs. + * + * @return quick, rough count of objects in this repository, {@code -1} if + * an exception occurs + * @since 6.1 + */ + public abstract long getApproximateObjectCount(); }