]> source.dussan.org Git - jgit.git/commitdiff
Handle global git config $XDG_CONFIG_HOME/git/config 48/203248/3
authorThomas Wolf <twolf@apache.org>
Wed, 5 Jul 2023 20:21:30 +0000 (22:21 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Mon, 28 Aug 2023 20:05:47 +0000 (22:05 +0200)
C git uses this alternate fallback location if the file exists and
~/.gitconfig does not. Implement this also for JGit.

If both files exist, reading behavior is as if the XDG config was
inserted between the HOME config and the system config. Writing
behaviour is different: all changes will be applied only in the HOME
config. Updates will occur in the XDG config only if the HOME config
does not exist.

This is consistent with the behavior of C git; compare [1], especially
the sections on FILES and SCOPES, and the description of the --global
option.

[1] https://git-scm.com/docs/git-config

Bug: 581875
Change-Id: I2460b9aa963fd2811ed8a5b77b05107d916f2b44
Signed-off-by: Thomas Wolf <twolf@apache.org>
org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java
org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java
org.eclipse.jgit/src/org/eclipse/jgit/storage/file/UserConfigFile.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java

diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java
new file mode 100644 (file)
index 0000000..7d212d5
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> 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.storage.file;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jgit.util.FS;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class UserConfigFileTest {
+
+       @Rule
+       public TemporaryFolder tmp = new TemporaryFolder();
+
+       @Test
+       public void testParentOnlyLoad() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+       }
+
+       @Test
+       public void testLoadBoth() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testOverwriteChild() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               config.setString("user", null, "name", "A U Thor");
+               assertEquals("A U Thor", config.getString("user", null, "name"));
+               config.save();
+               UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config2.load();
+               assertEquals("A U Thor", config2.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               FileBasedConfig cfg = new FileBasedConfig(null, xdg.toFile(),
+                               FS.DETECTED);
+               cfg.load();
+               assertEquals("Archibald Ulysses Thor",
+                               cfg.getString("user", null, "name"));
+               assertNull(cfg.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testUnset() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               config.setString("user", null, "name", "A U Thor");
+               assertEquals("A U Thor", config.getString("user", null, "name"));
+               config.unset("user", null, "name");
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               config.save();
+               UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config2.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config2.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               FileBasedConfig cfg = new FileBasedConfig(null, user.toFile(),
+                               FS.DETECTED);
+               cfg.load();
+               assertNull(cfg.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               cfg.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testUnsetSection() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               config.unsetSection("user", null);
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               config.save();
+               assertTrue(Files.readString(user).strip().isEmpty());
+       }
+
+       @Test
+       public void testNoChild() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertNull(config.getString("user", null, "email"));
+               config.setString("user", null, "email", "a.u.thor@example.com");
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               config.save();
+               assertFalse(Files.exists(user));
+               UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config2.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config2.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config2.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testNoFiles() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertNull(config.getString("user", null, "name"));
+               assertNull(config.getString("user", null, "email"));
+               config.setString("user", null, "name", "Archibald Ulysses Thor");
+               config.setString("user", null, "email", "a.u.thor@example.com");
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               config.save();
+               assertTrue(Files.exists(user));
+               assertFalse(Files.exists(xdg));
+               UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config2.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config2.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config2.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testSetInXdg() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               config.setString("user", null, "email", "a.u.thor@example.com");
+               config.save();
+               assertFalse(Files.exists(user));
+               FileBasedConfig cfg = new FileBasedConfig(null, xdg.toFile(),
+                               FS.DETECTED);
+               cfg.load();
+               assertEquals("Archibald Ulysses Thor",
+                               cfg.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               cfg.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testUserConfigCreated() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               Files.writeString(user,
+                               "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor");
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertTrue(config.isOutdated());
+               config.load();
+               assertEquals("A U Thor", config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testUserConfigDeleted() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Files.writeString(user,
+                               "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor");
+               Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("A U Thor", config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               Files.delete(user);
+               assertEquals("A U Thor", config.getString("user", null, "name"));
+               assertEquals("a.u.thor@example.com",
+                               config.getString("user", null, "email"));
+               assertTrue(config.isOutdated());
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertNull(config.getString("user", null, "email"));
+       }
+
+       @Test
+       public void testXdgConfigDeleted() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               Files.delete(xdg);
+               assertEquals("Archibald Ulysses Thor",
+                               config.getString("user", null, "name"));
+               assertTrue(config.isOutdated());
+               config.load();
+               assertNull(config.getString("user", null, "name"));
+       }
+
+       @Test
+       public void testXdgConfigDeletedUserConfigExists() throws Exception {
+               Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+               Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+               Path user = tmp.getRoot().toPath().resolve("user.cfg");
+               Files.writeString(user,
+                               "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor");
+               Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+               UserConfigFile config = new UserConfigFile(null, user.toFile(),
+                               xdg.toFile(), FS.DETECTED);
+               config.load();
+               assertEquals("A U Thor", config.getString("user", null, "name"));
+               Files.delete(xdg);
+               assertTrue(config.isOutdated());
+               config.load();
+               assertEquals("A U Thor", config.getString("user", null, "name"));
+       }
+
+}
index 0cccaec49229e74d08c4c65f114fff710f867b36..7e2c5b5ad0c5dac372209cd47bb10071fd805f8b 100644 (file)
@@ -739,7 +739,7 @@ public class Config {
                listeners.dispatch(new ConfigChangedEvent());
        }
 
-       String getRawString(final String section, final String subsection,
+       private String getRawString(final String section, final String subsection,
                        final String name) {
                String[] lst = getRawStringList(section, subsection, name);
                if (lst != null) {
index 80aceb4e7d1f3aef0ff4de1c7ffbc00a3b88b0c0..a71549c92e2d959cb5edb66f9b849b01647376ac 100644 (file)
@@ -34,7 +34,7 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter {
        @Override
        public boolean getBoolean(Config config, String section, String subsection,
                        String name, boolean defaultValue) {
-               String n = config.getRawString(section, subsection, name);
+               String n = config.getString(section, subsection, name);
                if (n == null) {
                        return defaultValue;
                }
index 910c5cbd85c195f17c4b34d43ef7507cd3ed2660..7fdcc4d3ecbffd5d79bd0efc8f628a77a9de4107 100644 (file)
@@ -22,6 +22,8 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.LockFailedException;
@@ -52,6 +54,8 @@ public class FileBasedConfig extends StoredConfig {
 
        private volatile ObjectId hash;
 
+       private AtomicBoolean exists = new AtomicBoolean();
+
        /**
         * Create a configuration with no default fallback.
         *
@@ -99,6 +103,21 @@ public class FileBasedConfig extends StoredConfig {
                return configFile;
        }
 
+       boolean exists() {
+               return exists.get();
+       }
+
+       @Override
+       public void setStringList(String section, String subsection, String name,
+                       List<String> values) {
+               super.setStringList(section, subsection, name, values);
+       }
+
+       @Override
+       public void unsetSection(String section, String subsection) {
+               super.unsetSection(section, subsection);
+       }
+
        /**
         * {@inheritDoc}
         * <p>
@@ -144,6 +163,7 @@ public class FileBasedConfig extends StoredConfig {
                                clear();
                                snapshot = lastSnapshot[0];
                        }
+                       exists.set(wasRead != null);
                } catch (IOException e) {
                        throw e;
                } catch (Exception e) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/UserConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/UserConfigFile.java
new file mode 100644 (file)
index 0000000..2ad74c2
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> 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.storage.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.util.FS;
+
+/**
+ * User (global) git config based on two possible locations,
+ * {@code ~/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config}.
+ * <p>
+ * For reading, both locations are considered, first the XDG file, then the file
+ * in the home directory. All updates occur in the last file read that exists,
+ * or in the home directory file if neither exists. In other words: if only the
+ * XDG file exists, it is updated, otherwise the home directory file is updated.
+ * </p>
+ *
+ * @since 6.7
+ */
+public class UserConfigFile extends FileBasedConfig {
+
+       private final FileBasedConfig parent;
+
+       /**
+        * Creates a new {@link UserConfigFile}.
+        *
+        * @param parent
+        *            parent {@link Config}; may be {@code null}
+        * @param config
+        *            {@link File} for {@code ~/.gitconfig}
+        * @param xdgConfig
+        *            {@link File} for {@code $XDG_CONFIG_HOME/.gitconfig}
+        * @param fileSystem
+        *            {@link FS} to use for the two files; normally
+        *            {@link FS#DETECTED}
+        */
+       public UserConfigFile(Config parent, @NonNull File config,
+                       @NonNull File xdgConfig, @NonNull FS fileSystem) {
+               super(new FileBasedConfig(parent, xdgConfig, fileSystem), config,
+                               fileSystem);
+               this.parent = (FileBasedConfig) getBaseConfig();
+       }
+
+       @Override
+       public void setStringList(String section, String subsection, String name,
+                       List<String> values) {
+               if (exists() || !parent.exists()) {
+                       super.setStringList(section, subsection, name, values);
+               } else {
+                       parent.setStringList(section, subsection, name, values);
+               }
+       }
+
+       @Override
+       public void unset(String section, String subsection, String name) {
+               if (exists() || !parent.exists()) {
+                       super.unset(section, subsection, name);
+               } else {
+                       parent.unset(section, subsection, name);
+               }
+       }
+
+       @Override
+       public void unsetSection(String section, String subsection) {
+               if (exists() || !parent.exists()) {
+                       super.unsetSection(section, subsection);
+               } else {
+                       parent.unsetSection(section, subsection);
+               }
+       }
+
+       @Override
+       public boolean isOutdated() {
+               return super.isOutdated() || parent.isOutdated();
+       }
+
+       @Override
+       public void load() throws IOException, ConfigInvalidException {
+               if (super.isOutdated()) {
+                       super.load();
+               }
+               if (parent.isOutdated()) {
+                       parent.load();
+               }
+       }
+
+       @Override
+       public void save() throws IOException {
+               if (exists() || !parent.exists()) {
+                       if (exists() || !toText().strip().isEmpty()) {
+                               super.save();
+                       }
+               } else {
+                       parent.save();
+               }
+       }
+}
index 991de51df73d6f22a6f54f5d72ac6178b07ba688..4a4876271070624bbe89a60e0546a2ca34ce1854 100644 (file)
@@ -39,6 +39,7 @@ import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectChecker;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.UserConfigFile;
 import org.eclipse.jgit.util.time.MonotonicClock;
 import org.eclipse.jgit.util.time.MonotonicSystemClock;
 import org.slf4j.Logger;
@@ -124,8 +125,15 @@ public abstract class SystemReader {
 
                @Override
                public FileBasedConfig openUserConfig(Config parent, FS fs) {
-                       return new FileBasedConfig(parent, new File(fs.userHome(), ".gitconfig"), //$NON-NLS-1$
-                                       fs);
+                       File homeFile = new File(fs.userHome(), ".gitconfig"); //$NON-NLS-1$
+                       Path xdgPath = getXdgConfigDirectory(fs);
+                       if (xdgPath != null) {
+                               Path configPath = xdgPath.resolve("git") //$NON-NLS-1$
+                                               .resolve(Constants.CONFIG);
+                               return new UserConfigFile(parent, homeFile, configPath.toFile(),
+                                               fs);
+                       }
+                       return new FileBasedConfig(parent, homeFile, fs);
                }
 
                @Override