Merge branch 'stable-5.5'

* stable-5.5:
  BaseReceivePack: Fix the format
  Prepend hostname to subsection used to store file timestamp resolution
  Store filesystem timestamp resolution in extra jgit config
  SystemReader: extract updating config and its parents if outdated

Change-Id: Iecfddce8081303af29badcdcd3d72a0da50c964f
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
index 5c2cd6a..eca8179 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
@@ -125,8 +125,9 @@ public abstract class LocalDiskRepositoryTestCase {
 	public void setUp() throws Exception {
 		tmp = File.createTempFile("jgit_test_", "_tmp");
 		CleanupThread.deleteOnShutdown(tmp);
-		if (!tmp.delete() || !tmp.mkdir())
+		if (!tmp.delete() || !tmp.mkdir()) {
 			throw new IOException("Cannot create " + tmp);
+		}
 
 		mockSystemReader = new MockSystemReader();
 		SystemReader.setInstance(mockSystemReader);
@@ -137,7 +138,11 @@ public void setUp() throws Exception {
 		// the same one here
 		FS.getFileStoreAttributes(tmp.toPath().getParent());
 
-		FileBasedConfig userConfig = new FileBasedConfig(
+		FileBasedConfig jgitConfig = new FileBasedConfig(
+				new File(tmp, "jgitconfig"), FS.DETECTED);
+		FileBasedConfig systemConfig = new FileBasedConfig(jgitConfig,
+				new File(tmp, "systemgitconfig"), FS.DETECTED);
+		FileBasedConfig userConfig = new FileBasedConfig(systemConfig,
 				new File(tmp, "usergitconfig"), FS.DETECTED);
 		// We have to set autoDetach to false for tests, because tests expect to be able
 		// to clean up by recursively removing the repository, and background GC might be
@@ -145,7 +150,10 @@ public void setUp() throws Exception {
 		userConfig.setBoolean(ConfigConstants.CONFIG_GC_SECTION,
 				null, ConfigConstants.CONFIG_KEY_AUTODETACH, false);
 		userConfig.save();
+		mockSystemReader.setJGitConfig(jgitConfig);
+		mockSystemReader.setSystemGitConfig(systemConfig);
 		mockSystemReader.setUserGitConfig(userConfig);
+
 		ceilTestDirectories(getCeilings());
 
 		author = new PersonIdent("J. Author", "jauthor@example.com");
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
index 13c2932..630c8a4 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
@@ -103,6 +103,8 @@ public String toString() {
 
 	private FileBasedConfig userGitConfig;
 
+	private FileBasedConfig jgitConfig;
+
 	FileBasedConfig systemGitConfig;
 
 	/**
@@ -119,6 +121,16 @@ public FileBasedConfig setUserGitConfig(FileBasedConfig userGitConfig) {
 	}
 
 	/**
+	 * Set the jgit config stored at $XDG_CONFIG_HOME/jgit/config
+	 *
+	 * @param jgitConfig
+	 *            set the jgit configuration
+	 */
+	public void setJGitConfig(FileBasedConfig jgitConfig) {
+		this.jgitConfig = jgitConfig;
+	}
+
+	/**
 	 * Set the system-level git config
 	 *
 	 * @param systemGitConfig
@@ -142,6 +154,7 @@ public MockSystemReader() {
 		init(Constants.GIT_COMMITTER_EMAIL_KEY);
 		setProperty(Constants.OS_USER_DIR, ".");
 		userGitConfig = new MockConfig(null, null);
+		jgitConfig = new MockConfig(null, null);
 		systemGitConfig = new MockConfig(null, null);
 		setCurrentPlatform();
 	}
@@ -200,6 +213,11 @@ public StoredConfig getUserConfig()
 	}
 
 	@Override
+	public FileBasedConfig getJGitConfig() {
+		return jgitConfig;
+	}
+
+	@Override
 	public StoredConfig getSystemConfig()
 			throws IOException, ConfigInvalidException {
 		return systemGitConfig;
@@ -333,4 +351,9 @@ public String toString() {
 		return "MockSystemReader";
 	}
 
+	@Override
+	public FileBasedConfig openJGitConfig(Config parent, FS fs) {
+		return jgitConfig;
+	}
+
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java
index 87349a2..d7a7d86 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java
@@ -59,7 +59,7 @@
 import org.junit.Test;
 
 public class FS_POSIXTest {
-	private SystemReader originalSystemReaderInstance;
+	private FileBasedConfig jgitConfig;
 
 	private FileBasedConfig systemConfig;
 
@@ -70,6 +70,7 @@ public class FS_POSIXTest {
 	@Before
 	public void setUp() throws Exception {
 		tmp = Files.createTempDirectory("jgit_test_");
+
 		MockSystemReader mockSystemReader = new MockSystemReader();
 		SystemReader.setInstance(mockSystemReader);
 
@@ -78,7 +79,10 @@ public void setUp() throws Exception {
 		// The MockSystemReader must be configured first since we need to use
 		// the same one here
 		FS.getFileStoreAttributes(tmp.getParent());
-		systemConfig = new FileBasedConfig(
+
+		jgitConfig = new FileBasedConfig(new File(tmp.toFile(), "jgitconfig"),
+				FS.DETECTED);
+		systemConfig = new FileBasedConfig(jgitConfig,
 				new File(tmp.toFile(), "systemgitconfig"), FS.DETECTED);
 		userConfig = new FileBasedConfig(systemConfig,
 				new File(tmp.toFile(), "usergitconfig"), FS.DETECTED);
@@ -89,16 +93,14 @@ public void setUp() throws Exception {
 		userConfig.setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
 				ConfigConstants.CONFIG_KEY_AUTODETACH, false);
 		userConfig.save();
+		mockSystemReader.setJGitConfig(jgitConfig);
 		mockSystemReader.setSystemGitConfig(systemConfig);
 		mockSystemReader.setUserGitConfig(userConfig);
-
-		originalSystemReaderInstance = SystemReader.getInstance();
-		SystemReader.setInstance(mockSystemReader);
 	}
 
 	@After
 	public void tearDown() throws IOException {
-		SystemReader.setInstance(originalSystemReaderInstance);
+		SystemReader.setInstance(null);
 		FileUtils.delete(tmp.toFile(), FileUtils.RECURSIVE | FileUtils.RETRY);
 	}
 
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 7391826..52eaa1f 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -201,8 +201,10 @@
 corruptPack=Pack file {0} is corrupt, removing it from pack list
 createBranchFailedUnknownReason=Create branch failed for unknown reason
 createBranchUnexpectedResult=Create branch returned unexpected result {0}
+createJGitConfigFailed=Creating JGit config directory {} failed
 createNewFileFailed=Could not create new file {0}
 createRequiresZeroOldId=Create requires old ID to be zero
+createXDGConfigHomeFailed=Creating XDG_CONFIG_HOME directory {} failed
 credentialPassword=Password
 credentialPassphrase=Passphrase
 credentialUsername=Username
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 2357e8e..43c2cc3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -262,8 +262,10 @@ public static JGitText get() {
 	/***/ public String countingObjects;
 	/***/ public String createBranchFailedUnknownReason;
 	/***/ public String createBranchUnexpectedResult;
+	/***/ public String createJGitConfigFailed;
 	/***/ public String createNewFileFailed;
 	/***/ public String createRequiresZeroOldId;
+	/***/ public String createXDGConfigHomeFailed;
 	/***/ public String credentialPassword;
 	/***/ public String credentialPassphrase;
 	/***/ public String credentialUsername;
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 938fa23..2ef3653 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
@@ -129,9 +129,21 @@ public Config(Config defaultConfig) {
 	}
 
 	/**
+	 * Retrieves this config's base config.
+	 *
+	 * @return the base configuration of this config.
+	 *
+	 * @since 5.5.2
+	 */
+	public Config getBaseConfig() {
+		return baseConfig;
+	}
+
+	/**
 	 * Check if a given string is the "missing" value.
 	 *
-	 * @param value string to be checked.
+	 * @param value
+	 *            string to be checked.
 	 * @return true if the given string is the "missing" value.
 	 * @since 5.4
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
index a084c82..9274fc6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
@@ -343,6 +343,15 @@ public final class Constants {
 	public static final String GIT_CONFIG_NOSYSTEM_KEY = "GIT_CONFIG_NOSYSTEM";
 
 	/**
+	 * The key of the XDG_CONFIG_HOME directory defined in the XDG base
+	 * directory specification, see
+	 * {@link "https://wiki.archlinux.org/index.php/XDG_Base_Directory"}
+	 *
+	 * @since 5.5.2
+	 */
+	public static final String XDG_CONFIG_HOME = "XDG_CONFIG_HOME";
+
+	/**
 	 * The environment variable that limits how close to the root of the file
 	 * systems JGit will traverse when looking for a repository root.
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
index d4fa17e..40c76cd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
@@ -1361,7 +1361,7 @@ private void parseShallow(String idStr) throws PackProtocolException {
 	}
 
 	static ReceiveCommand parseCommand(String line) throws PackProtocolException {
-          if (line == null || line.length() < 83) {
+		if (line == null || line.length() < 83) {
 			throw new PackProtocolException(
 					JGitText.get().errorInvalidProtocolWantedOldNewRef);
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
index 2035ada..b6c8850 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
@@ -246,8 +246,9 @@ private static void setBackground(boolean async) {
 			background.set(async);
 		}
 
-		private static final String javaVersionPrefix = System
-				.getProperty("java.vendor") + '|' //$NON-NLS-1$
+		private static final String javaVersionPrefix = SystemReader
+				.getInstance().getHostname() + '|'
+				+ System.getProperty("java.vendor") + '|' //$NON-NLS-1$
 				+ System.getProperty("java.version") + '|'; //$NON-NLS-1$
 
 		private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration
@@ -547,9 +548,9 @@ private static Optional<FileStoreAttributes> readFromConfig(
 
 		private static void saveToConfig(FileStore s,
 				FileStoreAttributes c) {
-			StoredConfig userConfig;
+			StoredConfig jgitConfig;
 			try {
-				userConfig = SystemReader.getInstance().getUserConfig();
+				jgitConfig = SystemReader.getInstance().getJGitConfig();
 			} catch (IOException | ConfigInvalidException e) {
 				LOG.error(JGitText.get().saveFileStoreAttributesFailed, e);
 				return;
@@ -570,20 +571,19 @@ private static void saveToConfig(FileStore s,
 			String key = getConfigKey(s);
 			while (!succeeded && retries < max_retries) {
 				try {
-					userConfig.load();
-					userConfig.setString(
+					jgitConfig.setString(
 							ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
 							ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
 							String.format("%d %s", //$NON-NLS-1$
 									Long.valueOf(resolutionValue),
 									resolutionUnit.name().toLowerCase()));
-					userConfig.setString(
+					jgitConfig.setString(
 							ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
 							ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
 							String.format("%d %s", //$NON-NLS-1$
 									Long.valueOf(minRacyThresholdValue),
 									minRacyThresholdUnit.name().toLowerCase()));
-					userConfig.save();
+					jgitConfig.save();
 					succeeded = true;
 				} catch (LockFailedException e) {
 					// race with another thread, wait a bit and try again
@@ -592,11 +592,11 @@ private static void saveToConfig(FileStore s,
 						if (retries < max_retries) {
 							Thread.sleep(100);
 							LOG.debug("locking {} failed, retries {}/{}", //$NON-NLS-1$
-									userConfig, Integer.valueOf(retries),
+									jgitConfig, Integer.valueOf(retries),
 									Integer.valueOf(max_retries));
 						} else {
 							LOG.warn(MessageFormat.format(
-									JGitText.get().lockFailedRetry, userConfig,
+									JGitText.get().lockFailedRetry, jgitConfig,
 									Integer.valueOf(retries)));
 						}
 					} catch (InterruptedException e1) {
@@ -605,12 +605,7 @@ private static void saveToConfig(FileStore s,
 					}
 				} catch (IOException e) {
 					LOG.error(MessageFormat.format(
-							JGitText.get().cannotSaveConfig, userConfig), e);
-					break;
-				} catch (ConfigInvalidException e) {
-					LOG.error(MessageFormat.format(
-							JGitText.get().repositoryConfigFileInvalid,
-							userConfig, e.getMessage()));
+							JGitText.get().cannotSaveConfig, jgitConfig), e);
 					break;
 				}
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
index b80ffb5..59184ff 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java
@@ -50,6 +50,10 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.text.DateFormat;
@@ -60,6 +64,7 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectChecker;
@@ -137,6 +142,42 @@ public FileBasedConfig openUserConfig(Config parent, FS fs) {
 					fs);
 		}
 
+		private Path getXDGConfigHome(FS fs) {
+			String configHomePath = getenv(Constants.XDG_CONFIG_HOME);
+			if (StringUtils.isEmptyOrNull(configHomePath)) {
+				configHomePath = new File(fs.userHome(), ".config") //$NON-NLS-1$
+						.getAbsolutePath();
+			}
+			try {
+				Path xdgHomePath = Paths.get(configHomePath);
+				Files.createDirectories(xdgHomePath);
+				return xdgHomePath;
+			} catch (IOException | InvalidPathException e) {
+				LOG.error(JGitText.get().createXDGConfigHomeFailed,
+						configHomePath, e);
+			}
+			return null;
+		}
+
+		@Override
+		public FileBasedConfig openJGitConfig(Config parent, FS fs) {
+			Path xdgPath = getXDGConfigHome(fs);
+			if (xdgPath != null) {
+				Path configPath = null;
+				try {
+					configPath = xdgPath.resolve("jgit"); //$NON-NLS-1$
+					Files.createDirectories(configPath);
+					configPath = configPath.resolve(Constants.CONFIG);
+					return new FileBasedConfig(parent, configPath.toFile(), fs);
+				} catch (IOException e) {
+					LOG.error(JGitText.get().createJGitConfigFailed, configPath,
+							e);
+				}
+			}
+			return new FileBasedConfig(parent,
+					new File(fs.userHome(), ".jgitconfig"), fs); //$NON-NLS-1$
+		}
+
 		@Override
 		public String getHostname() {
 			if (hostname == null) {
@@ -198,6 +239,8 @@ public static void setInstance(SystemReader newReader) {
 
 	private AtomicReference<FileBasedConfig> userConfig = new AtomicReference<>();
 
+	private AtomicReference<FileBasedConfig> jgitConfig = new AtomicReference<>();
+
 	private void init() {
 		// Creating ObjectChecker must be deferred. Unit tests change
 		// behavior of is{Windows,MacOS} in constructor of subclass.
@@ -275,6 +318,22 @@ protected final void setPlatformChecker() {
 	public abstract FileBasedConfig openSystemConfig(Config parent, FS fs);
 
 	/**
+	 * Open the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. Use
+	 * {@link #getJGitConfig()} to get the current jgit configuration in the
+	 * user home since it manages automatic reloading when the jgit config file
+	 * was modified and avoids unnecessary reloads.
+	 *
+	 * @param parent
+	 *            a config with values not found directly in the returned config
+	 * @param fs
+	 *            the file system abstraction which will be necessary to perform
+	 *            certain file system operations.
+	 * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
+	 * @since 5.5.2
+	 */
+	public abstract FileBasedConfig openJGitConfig(Config parent, FS fs);
+
+	/**
 	 * Get the git configuration found in the user home. The configuration will
 	 * be reloaded automatically if the configuration file was modified. Also
 	 * reloads the system config if the system config file was modified. If the
@@ -288,20 +347,41 @@ protected final void setPlatformChecker() {
 	 * @since 5.1.9
 	 */
 	public StoredConfig getUserConfig()
-			throws IOException, ConfigInvalidException {
+			throws ConfigInvalidException, IOException {
 		FileBasedConfig c = userConfig.get();
 		if (c == null) {
 			userConfig.compareAndSet(null,
 					openUserConfig(getSystemConfig(), FS.DETECTED));
 			c = userConfig.get();
-		} else {
-			// Ensure the parent is up to date
-			getSystemConfig();
 		}
-		if (c.isOutdated()) {
-			LOG.debug("loading user config {}", userConfig); //$NON-NLS-1$
-			c.load();
+		// on the very first call this will check a second time if the system
+		// config is outdated
+		updateAll(c);
+		return c;
+	}
+
+	/**
+	 * Get the jgit configuration located at $XDG_CONFIG_HOME/jgit/config. The
+	 * configuration will be reloaded automatically if the configuration file
+	 * was modified. If the configuration file wasn't modified returns the
+	 * cached configuration.
+	 *
+	 * @return the jgit configuration located at $XDG_CONFIG_HOME/jgit/config
+	 * @throws ConfigInvalidException
+	 *             if configuration is invalid
+	 * @throws IOException
+	 *             if something went wrong when reading files
+	 * @since 5.5.2
+	 */
+	public StoredConfig getJGitConfig()
+			throws ConfigInvalidException, IOException {
+		FileBasedConfig c = jgitConfig.get();
+		if (c == null) {
+			jgitConfig.compareAndSet(null,
+					openJGitConfig(null, FS.DETECTED));
+			c = jgitConfig.get();
 		}
+		updateAll(c);
 		return c;
 	}
 
@@ -319,21 +399,46 @@ public StoredConfig getUserConfig()
 	 * @since 5.1.9
 	 */
 	public StoredConfig getSystemConfig()
-			throws IOException, ConfigInvalidException {
+			throws ConfigInvalidException, IOException {
 		FileBasedConfig c = systemConfig.get();
 		if (c == null) {
 			systemConfig.compareAndSet(null,
-					openSystemConfig(null, FS.DETECTED));
+					openSystemConfig(getJGitConfig(), FS.DETECTED));
 			c = systemConfig.get();
 		}
-		if (c.isOutdated()) {
-			LOG.debug("loading system config {}", systemConfig); //$NON-NLS-1$
-			c.load();
-		}
+		updateAll(c);
 		return c;
 	}
 
 	/**
+	 * Update config and its parents if they seem modified
+	 *
+	 * @param config
+	 *            configuration to reload if outdated
+	 * @throws ConfigInvalidException
+	 *             if configuration is invalid
+	 * @throws IOException
+	 *             if something went wrong when reading files
+	 */
+	private void updateAll(Config config)
+			throws ConfigInvalidException, IOException {
+		if (config == null) {
+			return;
+		}
+		updateAll(config.getBaseConfig());
+		if (config instanceof FileBasedConfig) {
+			FileBasedConfig cfg = (FileBasedConfig) config;
+			if (!cfg.getFile().exists()) {
+				return;
+			}
+			if (cfg.isOutdated()) {
+				LOG.debug("loading config {}", cfg); //$NON-NLS-1$
+				cfg.load();
+			}
+		}
+	}
+
+	/**
 	 * Get the current system time
 	 *
 	 * @return the current system time