Merge branch 'master' into stable-6.1

* master:
  Describe: add support for core.abbrev config option
  Add a typed config getter for integers confined to a range
  Remove odd prefix of PersonIdent test class
  PersonIdent: Add ctors that accept Instant in addition to Date
  Remove ignored potentiallyUnclosedCloseable check
  Make precedence more explicit
  [pgm] Add describe --abbrev option
  Cap describe abbrev option
  DescribeCommand: Add support for --abbrev=0

Change-Id: I1daa3501a38d57b628800fadb96b6b71eea8cbb3
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
index 4bad73b..c785443 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
@@ -89,6 +89,31 @@
 	}
 
 	@Test
+	public void testDescribeCommitMatchAbbrev() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		assertArrayEquals(new String[] { "v1.0-1-g56f6cebdf3f5", "" },
+				execute("git describe --abbrev 12 --match v1.*"));
+	}
+
+	@Test
+	public void testDescribeCommitMatchAbbrevMin() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		assertArrayEquals(new String[] { "v1.0-1-g56f6", "" },
+				execute("git describe --abbrev -5 --match v1.*"));
+	}
+
+	@Test
+	public void testDescribeCommitMatchAbbrevMax() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		assertArrayEquals(new String[] {
+				"v1.0-1-g56f6cebdf3f5ceeecd803365abf0996fb1fa006d", "" },
+				execute("git describe --abbrev 50 --match v1.*"));
+	}
+
+	@Test
 	public void testDescribeCommitMatch2() throws Exception {
 		initialCommitAndTag();
 		secondCommit();
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index d51daaf..fda0bf6 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -229,6 +229,7 @@
 unsupportedOperation=Unsupported operation: {0}
 untrackedFiles=Untracked files:
 updating=Updating {0}..{1}
+usage_Abbrev=Instead of using the default number of hexadecimal digits (which will vary according to the number of objects in the repository with a default of 7) of the abbreviated object name, use <n> digits, or as many digits as needed to form a unique object name. An <n> of 0 will suppress long format, only showing the closest tag.
 usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time
 usage_AlwaysFallback=Show uniquely abbreviated commit object as fallback
 usage_bareClone=Make a bare Git repository. That is, instead of creating [DIRECTORY] and placing the administrative files in [DIRECTORY]/.git, make the [DIRECTORY] itself the $GIT_DIR.
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
index 8aa119a..116db03 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
@@ -44,6 +44,9 @@
 	@Option(name = "--match", usage = "usage_Match", metaVar = "metaVar_pattern")
 	private List<String> patterns = new ArrayList<>();
 
+	@Option(name = "--abbrev", usage = "usage_Abbrev")
+	private Integer abbrev;
+
 	/** {@inheritDoc} */
 	@Override
 	protected void run() {
@@ -57,6 +60,9 @@
 			cmd.setTags(useTags);
 			cmd.setAlways(always);
 			cmd.setMatch(patterns.toArray(new String[0]));
+			if (abbrev != null) {
+				cmd.setAbbrev(abbrev.intValue());
+			}
 			String result = null;
 			try {
 				result = cmd.call();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
index 2051169..ab87fa9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
@@ -14,7 +14,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import java.io.BufferedWriter;
@@ -102,6 +101,12 @@
 			assertEquals("alice-t1-2-g3e563c5", describe(c4, "alice*"));
 			assertEquals("bob-t2-1-g3e563c5", describe(c4, "bob*"));
 			assertEquals("bob-t2-1-g3e563c5", describe(c4, "a*", "b*", "c*"));
+
+			assertEquals("bob-t2", describe(c4, false, true, 0));
+			assertEquals("bob-t2-1-g3e56", describe(c4, false, true, 1));
+			assertEquals("bob-t2-1-g3e56", describe(c4, false, true, -10));
+			assertEquals("bob-t2-1-g3e563c55927905f21e3bc7c00a3d83a31bf4ed3a",
+					describe(c4, false, true, 50));
 		} else {
 			assertEquals(null, describe(c2));
 			assertEquals(null, describe(c3));
@@ -115,16 +120,13 @@
 			assertEquals("44579ebe7f", describe(c3, false, true, 10));
 			assertEquals("3e563c5592", describe(c4, false, true, 10));
 
-			assertEquals("3e", describe(c4, false, true, 2));
+			assertEquals("3e56", describe(c4, false, true, -10));
+			assertEquals("3e56", describe(c4, false, true, 0));
+			assertEquals("3e56", describe(c4, false, true, 2));
 			assertEquals("3e563c55927905f21e3bc7c00a3d83a31bf4ed3a",
 					describe(c4, false, true, 40));
-
-			assertThrows(StringIndexOutOfBoundsException.class,
-					() -> describe(c4, false, true, -10));
-			assertThrows(StringIndexOutOfBoundsException.class,
-					() -> describe(c4, false, true, 1));
-			assertThrows(StringIndexOutOfBoundsException.class,
-					() -> describe(c4, false, true, 41));
+			assertEquals("3e563c55927905f21e3bc7c00a3d83a31bf4ed3a",
+					describe(c4, false, true, 42));
 		}
 
 		// test default target
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
index bacd3ba..ab588cb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
@@ -269,7 +269,6 @@
 		assertEquals(1, evicted.get());
 	}
 
-	@SuppressWarnings("resource")
 	@Test
 	public void noConcurrencySerializedReads_oneRepo() throws Exception {
 		InMemoryRepository r1 = createRepoWithBitmap("test");
@@ -384,7 +383,6 @@
 		assertEquals(2, cache.getMissCount()[0]);
 	}
 
-	@SuppressWarnings("resource")
 	@Test
 	public void highConcurrencyParallelReads_oneRepo() throws Exception {
 		InMemoryRepository r1 = createRepoWithBitmap("test");
@@ -407,7 +405,6 @@
 		assertEquals(1, cache.getMissCount()[0]);
 	}
 
-	@SuppressWarnings("resource")
 	@Test
 	public void highConcurrencyParallelReads_oneRepoParallelReverseIndex()
 			throws Exception {
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 0000000..96ace08
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022, Matthias Sohn <matthias.sohn@sap.com> 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.test/tst/org/eclipse/jgit/lib/T0001_PersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
similarity index 75%
rename from org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0001_PersonIdentTest.java
rename to org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
index e9bab7c..97da175 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0001_PersonIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
@@ -13,12 +13,14 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import java.time.Instant;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.TimeZone;
 
 import org.junit.Test;
 
-public class T0001_PersonIdentTest {
+public class PersonIdentTest {
 
 	@Test
 	public void test001_NewIdent() {
@@ -42,6 +44,34 @@
 				p.toExternalString());
 	}
 
+	@Test
+	public void testNewIdentInstant() {
+		PersonIdent p = new PersonIdent("A U Thor", "author@example.com",
+				Instant.ofEpochMilli(1142878501000L),
+				ZoneId.of("America/New_York"));
+		assertEquals("A U Thor", p.getName());
+		assertEquals("author@example.com", p.getEmailAddress());
+		assertEquals(Instant.ofEpochMilli(1142878501000L),
+				p.getWhenAsInstant());
+		assertEquals("A U Thor <author@example.com> 1142878501 -0500",
+				p.toExternalString());
+		assertEquals(ZoneId.of("GMT-05:00"), p.getZoneId());
+	}
+
+	@Test
+	public void testNewIdentInstant2() {
+		final PersonIdent p = new PersonIdent("A U Thor", "author@example.com",
+				Instant.ofEpochMilli(1142878501000L),
+				ZoneId.of("Asia/Kolkata"));
+		assertEquals("A U Thor", p.getName());
+		assertEquals("author@example.com", p.getEmailAddress());
+		assertEquals(Instant.ofEpochMilli(1142878501000L),
+				p.getWhenAsInstant());
+		assertEquals("A U Thor <author@example.com> 1142878501 +0530",
+				p.toExternalString());
+		assertEquals(ZoneId.of("GMT+05:30"), p.getZoneId());
+	}
+
 	@SuppressWarnings("unused")
 	@Test(expected = IllegalArgumentException.class)
 	public void nullForNameShouldThrowIllegalArgumentException() {
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 792a0c9..e026e31 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -9,6 +9,36 @@
             </message_arguments>
         </filter>
     </resource>
+    <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter">
+        <filter id="403767336">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="UNSET_INT"/>
+            </message_arguments>
+        </filter>
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getIntInRange(Config, String, String, String, int, int, int)"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/lib/ObjectDatabase.java" type="org.eclipse.jgit.lib.ObjectDatabase">
+        <filter id="336695337">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.ObjectDatabase"/>
+                <message_argument value="getApproximateObjectCount()"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter">
+        <filter id="403804204">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/>
+                <message_argument value="getIntInRange(Config, String, String, String, int, int, int)"/>
+            </message_arguments>
+        </filter>
+    </resource>
     <resource path="src/org/eclipse/jgit/transport/BasePackPushConnection.java" type="org.eclipse.jgit.transport.BasePackPushConnection">
         <filter id="338792546">
             <message_arguments>
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 31579c9..e6f4e65 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -354,6 +354,8 @@
 inMemoryBufferLimitExceeded=In-memory buffer limit exceeded
 inputDidntMatchLength=Input did not match supplied length. {0} bytes are missing.
 inputStreamMustSupportMark=InputStream must support mark()
+integerValueNotInRange=Integer value {0}.{1} = {2} not in range {3}..{4}
+integerValueNotInRangeSubSection=Integer value {0}.{1}.{2} = {3} not in range {4}..{5}
 integerValueOutOfRange=Integer value {0}.{1} out of range
 internalRevisionError=internal revision error
 internalServerError=internal server error
@@ -364,6 +366,7 @@
 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 e572773..805a886 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 @@
 	/**
 	 * 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,17 +217,25 @@
 	 *
 	 * @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;
 	}
 
 	private String longDescription(Ref tag, int depth, ObjectId tip)
 			throws IOException {
+		if (abbrev == 0) {
+			return formatRefName(tag.getName());
+		}
 		return String.format("%s-%d-g%s", formatRefName(tag.getName()), //$NON-NLS-1$
 				Integer.valueOf(depth),
 				w.getObjectReader().abbreviate(tip, abbrev).name());
@@ -321,6 +330,9 @@
 			if (target == null) {
 				setTarget(Constants.HEAD);
 			}
+			if (abbrev == UNSET_INT) {
+				abbrev = AbbrevConfig.parseFromConfig(repo).get();
+			}
 
 			Collection<Ref> tagList = repo.getRefDatabase()
 					.getRefsByPrefix(useAll ? R_REFS : R_TAGS);
@@ -433,7 +445,10 @@
 			// if all the nodes are dominated by all the tags, the walk stops
 			if (candidates.isEmpty()) {
 				return always
-						? w.getObjectReader().abbreviate(target, abbrev).name()
+						? w.getObjectReader()
+								.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 58615b4..16b3f37 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -382,6 +382,8 @@
 	/***/ public String inMemoryBufferLimitExceeded;
 	/***/ public String inputDidntMatchLength;
 	/***/ public String inputStreamMustSupportMark;
+	/***/ public String integerValueNotInRange;
+	/***/ public String integerValueNotInRangeSubSection;
 	/***/ public String integerValueOutOfRange;
 	/***/ public String internalRevisionError;
 	/***/ public String internalServerError;
@@ -392,6 +394,7 @@
 	/***/ 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 5b6894d..99da222 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 @@
 				}
 			};
 		}
+
+		@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 7dedeb5..094fdc1 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 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 627facc..06c8cad 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 @@
 		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}
 	 * <p>
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 0000000..9109cfd
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022,  Matthias Sohn <matthias.sohn@sap.com> 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 <a
+ * href=https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreabbrev">
+ * core.abbrev</a>
+ *
+ * @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 1ce3e31..d1d66d2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
@@ -278,6 +278,54 @@
 	}
 
 	/**
+	 * 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.
 	 *
 	 * @param section
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 42d8aa5..80d720a 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 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/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
index 9f96bce..8640940 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
@@ -120,6 +120,26 @@
 
 	/** {@inheritDoc} */
 	@Override
+	public int getIntInRange(Config config, String section, String subsection,
+			String name, int minValue, int maxValue, int defaultValue) {
+		int val = getInt(config, section, subsection, name, defaultValue);
+		if ((val >= minValue && val <= maxValue) || val == UNSET_INT) {
+			return val;
+		}
+		if (subsection == null) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					JGitText.get().integerValueNotInRange, section, name,
+					Integer.valueOf(val), Integer.valueOf(minValue),
+					Integer.valueOf(maxValue)));
+		}
+		throw new IllegalArgumentException(MessageFormat.format(
+				JGitText.get().integerValueNotInRangeSubSection, section,
+				subsection, name, Integer.valueOf(val),
+				Integer.valueOf(minValue), Integer.valueOf(maxValue)));
+	}
+
+	/** {@inheritDoc} */
+	@Override
 	public long getLong(Config config, String section, String subsection,
 			String name, long defaultValue) {
 		final String str = config.getString(section, subsection, name);
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 04262c0..70009cb 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 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();
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
index 428a6b9..9371029 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
@@ -14,6 +14,8 @@
 
 import java.io.Serializable;
 import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.Locale;
 import java.util.TimeZone;
@@ -206,6 +208,20 @@
 	}
 
 	/**
+	 * Copy a {@link org.eclipse.jgit.lib.PersonIdent}, but alter the clone's
+	 * time stamp
+	 *
+	 * @param pi
+	 *            original {@link org.eclipse.jgit.lib.PersonIdent}
+	 * @param aWhen
+	 *            local time as Instant
+	 * @since 6.1
+	 */
+	public PersonIdent(PersonIdent pi, Instant aWhen) {
+		this(pi.getName(), pi.getEmailAddress(), aWhen.toEpochMilli(), pi.tzOffset);
+	}
+
+	/**
 	 * Construct a PersonIdent from simple data
 	 *
 	 * @param aName a {@link java.lang.String} object.
@@ -222,6 +238,27 @@
 	}
 
 	/**
+	 * Construct a PersonIdent from simple data
+	 *
+	 * @param aName
+	 *            a {@link java.lang.String} object.
+	 * @param aEmailAddress
+	 *            a {@link java.lang.String} object.
+	 * @param aWhen
+	 *            local time stamp
+	 * @param zoneId
+	 *            time zone id
+	 * @since 6.1
+	 */
+	public PersonIdent(final String aName, String aEmailAddress, Instant aWhen,
+			ZoneId zoneId) {
+		this(aName, aEmailAddress, aWhen.toEpochMilli(),
+				TimeZone.getTimeZone(zoneId)
+						.getOffset(aWhen
+				.toEpochMilli()) / (60 * 1000));
+	}
+
+	/**
 	 * Copy a PersonIdent, but alter the clone's time stamp
 	 *
 	 * @param pi
@@ -304,6 +341,16 @@
 	}
 
 	/**
+	 * Get when attribute as instant
+	 *
+	 * @return timestamp
+	 * @since 6.1
+	 */
+	public Instant getWhenAsInstant() {
+		return Instant.ofEpochMilli(when);
+	}
+
+	/**
 	 * Get this person's declared time zone
 	 *
 	 * @return this person's declared time zone; null if time zone is unknown.
@@ -313,6 +360,16 @@
 	}
 
 	/**
+	 * Get the time zone id
+	 *
+	 * @return the time zone id
+	 * @since 6.1
+	 */
+	public ZoneId getZoneId() {
+		return getTimeZone().toZoneId();
+	}
+
+	/**
 	 * Get this person's declared time zone as minutes east of UTC.
 	 *
 	 * @return this person's declared time zone as minutes east of UTC. If the
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
index 0f2f6cf..c4eb8f1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java
@@ -29,6 +29,13 @@
 public interface TypedConfigGetter {
 
 	/**
+	 * Use {@code Integer#MIN_VALUE} as unset int value
+	 *
+	 * @since 6.1
+	 */
+	public static final int UNSET_INT = Integer.MIN_VALUE;
+
+	/**
 	 * Get a boolean value from a git {@link Config}.
 	 *
 	 * @param config
@@ -87,6 +94,32 @@
 			int defaultValue);
 
 	/**
+	 * Obtain an integer value from a git {@link Config} which must be in given
+	 * range.
+	 *
+	 * @param config
+	 *            to get the value from
+	 * @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
+	 *            minimal value
+	 * @param maxValue
+	 *            maximum value
+	 * @param defaultValue
+	 *            default value to return if no value was present. Use
+	 *            {@code #UNSET_INT} to set the default to unset.
+	 * @return an integer value from the configuration, or defaultValue.
+	 *         {@code #UNSET_INT} if unset.
+	 * @since 6.1
+	 */
+	int getIntInRange(Config config, String section, String subsection,
+			String name, int minValue, int maxValue, int defaultValue);
+
+	/**
 	 * Obtain a long value from a git {@link Config}.
 	 *
 	 * @param config
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java
index 0de2702..c8774d5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java
@@ -117,7 +117,7 @@
 		@Override
 		public boolean matchConfigValue(String in) {
 			return toConfigValue().equalsIgnoreCase(in)
-					|| alias != null && alias.equalsIgnoreCase(in);
+					|| (alias != null && alias.equalsIgnoreCase(in));
 		}
 	}