]> source.dussan.org Git - jgit.git/commitdiff
Add symlink support to JGit 79/9379/38
authorRobin Rosenberg <robin.rosenberg@dewire.com>
Fri, 4 Jan 2013 23:34:03 +0000 (00:34 +0100)
committerMatthias Sohn <matthias.sohn@sap.com>
Mon, 10 Feb 2014 21:53:33 +0000 (22:53 +0100)
The change includes comparing symbolic links between disk and index,
adding symbolic links to the index, creating/modifying links on
checkout. The behavior is controlled by the core.symlinks setting, just
as C Git does. When a new repository is created core.symlinks will be
set depending on the capabilities of the operating system and Java
runtime.

If core.symlinks is set to true, the assumption is that symlinks are
supported, which may result in runtime errors if this turns out not to
be the case.

Measuring the cost of jgit status on a repository with ~70000 files,
of which ~30000 are tracked reveals a penalty of about 10% for using
the Java7 (really NIO2) support module.

Bug: 354367
Change-Id: I12f0fdd9d26212324a586896ef7eb1f6ff89c39c
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
14 files changed:
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ConfigTest.java
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/CleanCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java
org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeOptions.java

index e869e8556800944abac322ca5eccc95cf6242b8d..3c62e85502f7cc06316d63464e29df86f2faa044 100644 (file)
@@ -46,6 +46,8 @@ import static org.junit.Assert.assertArrayEquals;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
@@ -75,8 +77,28 @@ public class ConfigTest extends CLIRepositoryTestCase {
                if (isMac)
                        expect.add("core.precomposeunicode=true");
                expect.add("core.repositoryformatversion=0");
+               if (SystemReader.getInstance().isWindows() && osVersion() < 6
+                               || javaVersion() < 1.7) {
+                       expect.add("core.symlinks=false");
+               }
                expect.add(""); // ends with LF (last line empty)
                assertArrayEquals("expected default configuration", expect.toArray(),
                                output);
        }
+
+       private static float javaVersion() {
+               String versionString = System.getProperty("java.version");
+               Matcher matcher = Pattern.compile("(\\d+\\.\\d+).*").matcher(
+                               versionString);
+               matcher.matches();
+               return Float.parseFloat(matcher.group(1));
+       }
+
+       private static float osVersion() {
+               String versionString = System.getProperty("os.version");
+               Matcher matcher = Pattern.compile("(\\d+\\.\\d+).*").matcher(
+                               versionString);
+               matcher.matches();
+               return Float.parseFloat(matcher.group(1));
+       }
 }
index 286710ec9105b1946d7bbeefa891ca4f149a14b2..164da3f1dbc61e7249a2660ad8851d2e0e4c3170 100644 (file)
@@ -181,7 +181,7 @@ class Blame extends TextBuiltin {
                                                generator.push(null, dc.getEntry(entry).getObjectId());
 
                                        File inTree = new File(db.getWorkTree(), file);
-                                       if (inTree.isFile())
+                                       if (db.getFS().isFile(inTree))
                                                generator.push(null, new RawText(inTree));
                                }
                        }
index 0bd1e9a9209ffa0d75fdd51b5dfcf1c180eb8abe..34ea5842d16815f9b3e6f22d804e3c3e0d776a6a 100644 (file)
@@ -63,6 +63,7 @@ import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
@@ -242,9 +243,11 @@ public class FileTreeIteratorTest extends RepositoryTestCase {
        }
 
        @Test
-       public void testIsModifiedSymlink() throws Exception {
+       public void testIsModifiedSymlinkAsFile() throws Exception {
                File f = writeTrashFile("symlink", "content");
                Git git = new Git(db);
+               db.getConfig().setString(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_SYMLINKS, "false");
                git.add().addFilepattern("symlink").call();
                git.commit().setMessage("commit").call();
 
index 11dfd1c585b33a951803b0222b6691cd9f70fe82..29726146c364b40c4d396e4d22b0ed8c0ca26ef1 100644 (file)
@@ -215,7 +215,7 @@ public class BlameCommand extends GitCommand<BlameResult> {
                                                gen.push(null, dc.getEntry(entry).getObjectId());
 
                                        File inTree = new File(repo.getWorkTree(), path);
-                                       if (inTree.isFile())
+                                       if (repo.getFS().isFile(inTree))
                                                gen.push(null, new RawText(inTree));
                                }
                        }
index f273eafe1f6d87fe062c915fa0a2e793aa97decd..59671281130d31843ced9b29a65f74ef3db89176 100644 (file)
@@ -53,6 +53,7 @@ import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 
 /**
@@ -100,13 +101,13 @@ public class CleanCommand extends GitCommand<Set<String>> {
                        Set<String> untrackedAndIgnoredDirs = new TreeSet<String>(
                                        status.getUntrackedFolders());
 
+                       FS fs = getRepository().getFS();
                        for (String p : status.getIgnoredNotInIndex()) {
                                File f = new File(repo.getWorkTree(), p);
-                               if (f.isFile()) {
+                               if (fs.isFile(f) || fs.isSymLink(f))
                                        untrackedAndIgnoredFiles.add(p);
-                               } else if (f.isDirectory()) {
+                               else if (fs.isDirectory(f))
                                        untrackedAndIgnoredDirs.add(p);
-                               }
                        }
 
                        Set<String> filtered = filterFolders(untrackedAndIgnoredFiles,
index e930c535e6e2bfd2edf05af8c02fae2827082adf..cb1e6cf14739139f1934aba9160a32a7f94f4339 100644 (file)
@@ -731,7 +731,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
                                List<String> fileList = dco.getToBeDeleted();
                                for (String filePath : fileList) {
                                        File fileToDelete = new File(repo.getWorkTree(), filePath);
-                                       if (fileToDelete.exists())
+                                       if (repo.getFS().exists(fileToDelete))
                                                FileUtils.delete(fileToDelete, FileUtils.RECURSIVE
                                                                | FileUtils.RETRY);
                                }
index 3f1afd7cc0450150c7fc07fa6fb8d581ce51b1d6..79fe8d6f567d9b17d809ce0413e13d3dcc7bf506 100644 (file)
@@ -60,6 +60,7 @@ import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
+import org.eclipse.jgit.lib.CoreConfig.SymLinks;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
@@ -399,7 +400,6 @@ public class DirCacheCheckout {
                        MissingObjectException, IncorrectObjectTypeException,
                        CheckoutConflictException, IndexWriteException {
                toBeDeleted.clear();
-
                ObjectReader objectReader = repo.getObjectDatabase().newReader();
                try {
                        if (headCommitTree != null)
@@ -425,13 +425,13 @@ public class DirCacheCheckout {
                        for (int i = removed.size() - 1; i >= 0; i--) {
                                String r = removed.get(i);
                                file = new File(repo.getWorkTree(), r);
-                               if (!file.delete() && file.exists()) {
+                               if (!file.delete() && repo.getFS().exists(file)) {
                                        // The list of stuff to delete comes from the index
                                        // which will only contain a directory if it is
                                        // a submodule, in which case we shall not attempt
                                        // to delete it. A submodule is not empty, so it
                                        // is safe to check this after a failed delete.
-                                       if (!file.isDirectory())
+                                       if (!repo.getFS().isDirectory(file))
                                                toBeDeleted.add(r);
                                } else {
                                        if (last != null && !isSamePrefix(r, last))
@@ -583,9 +583,8 @@ public class DirCacheCheckout {
                // represents the state for the merge iterator, the second last the
                // state for the index iterator and the third last represents the state
                // for the head iterator. The hexadecimal constant "F" stands for
-               // "file",
-               // an "D" stands for "directory" (tree), and a "0" stands for
-               // non-existing
+               // "file", a "D" stands for "directory" (tree), and a "0" stands for
+               // non-existing. Symbolic links and git links are treated as File here.
                //
                // Examples:
                // ffMask == 0xFFD -> Head=File, Index=File, Merge=Tree
@@ -1117,35 +1116,45 @@ public class DirCacheCheckout {
                ObjectLoader ol = or.open(entry.getObjectId());
                File parentDir = f.getParentFile();
                parentDir.mkdirs();
-               File tmpFile = File.createTempFile("._" + f.getName(), null, parentDir); //$NON-NLS-1$
-               WorkingTreeOptions opt = repo.getConfig().get(WorkingTreeOptions.KEY);
-               FileOutputStream rawChannel = new FileOutputStream(tmpFile);
-               OutputStream channel;
-               if (opt.getAutoCRLF() == AutoCRLF.TRUE)
-                       channel = new AutoCRLFOutputStream(rawChannel);
-               else
-                       channel = rawChannel;
-               try {
-                       ol.copyTo(channel);
-               } finally {
-                       channel.close();
-               }
                FS fs = repo.getFS();
-               if (opt.isFileMode() && fs.supportsExecute()) {
-                       if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) {
-                               if (!fs.canExecute(tmpFile))
-                                       fs.setExecute(tmpFile, true);
-                       } else {
-                               if (fs.canExecute(tmpFile))
-                                       fs.setExecute(tmpFile, false);
+               WorkingTreeOptions opt = repo.getConfig().get(WorkingTreeOptions.KEY);
+               if (entry.getFileMode() == FileMode.SYMLINK
+                               && opt.getSymLinks() == SymLinks.TRUE) {
+                       byte[] bytes = ol.getBytes();
+                       String target = RawParseUtils.decode(bytes);
+                       fs.createSymLink(f, target);
+                       entry.setLength(bytes.length);
+                       entry.setLastModified(fs.lastModified(f));
+               } else {
+                       File tmpFile = File.createTempFile(
+                                       "._" + f.getName(), null, parentDir); //$NON-NLS-1$
+                       FileOutputStream rawChannel = new FileOutputStream(tmpFile);
+                       OutputStream channel;
+                       if (opt.getAutoCRLF() == AutoCRLF.TRUE)
+                               channel = new AutoCRLFOutputStream(rawChannel);
+                       else
+                               channel = rawChannel;
+                       try {
+                               ol.copyTo(channel);
+                       } finally {
+                               channel.close();
+                       }
+                       if (opt.isFileMode() && fs.supportsExecute()) {
+                               if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) {
+                                       if (!fs.canExecute(tmpFile))
+                                               fs.setExecute(tmpFile, true);
+                               } else {
+                                       if (fs.canExecute(tmpFile))
+                                               fs.setExecute(tmpFile, false);
+                               }
+                       }
+                       try {
+                               FileUtils.rename(tmpFile, f);
+                       } catch (IOException e) {
+                               throw new IOException(MessageFormat.format(
+                                               JGitText.get().couldNotWriteFile, tmpFile.getPath(),
+                                               f.getPath()));
                        }
-               }
-               try {
-                       FileUtils.rename(tmpFile, f);
-               } catch (IOException e) {
-                       throw new IOException(MessageFormat.format(
-                                       JGitText.get().couldNotWriteFile, tmpFile.getPath(),
-                                       f.getPath()));
                }
                entry.setLastModified(f.lastModified());
                if (opt.getAutoCRLF() != AutoCRLF.FALSE)
index 148781dfe9111a6f00a59ca81ab9fad67f353397..4c420f693c707b8828adf87959e35bedc45cfc38 100644 (file)
@@ -64,6 +64,7 @@ import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepositor
 import org.eclipse.jgit.lib.BaseRepositoryBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.SymLinks;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
@@ -293,6 +294,21 @@ public class FileRepository extends Repository {
                        fileMode = false;
                }
 
+               SymLinks symLinks = SymLinks.FALSE;
+               if (getFS().supportsSymlinks()) {
+                       File tmp = new File(getDirectory(), "tmplink"); //$NON-NLS-1$
+                       try {
+                               getFS().createSymLink(tmp, "target"); //$NON-NLS-1$
+                               symLinks = null;
+                               FileUtils.delete(tmp);
+                       } catch (IOException e) {
+                               // Normally a java.nio.file.FileSystemException
+                       }
+               }
+               if (symLinks != null)
+                       cfg.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
+                                       ConfigConstants.CONFIG_KEY_SYMLINKS, symLinks.name()
+                                                       .toLowerCase());
                cfg.setInt(ConfigConstants.CONFIG_CORE_SECTION, null,
                                ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
                cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
index fd22764b6a8c6a8107af443f30b50ad14b5e903f..bca79f27102a855263dcb9a3e163f3a6a824f4f5 100644 (file)
@@ -123,6 +123,13 @@ public class ConfigConstants {
        /** The "deltaBaseCacheLimit" key */
        public static final String CONFIG_KEY_DELTA_BASE_CACHE_LIMIT = "deltaBaseCacheLimit";
 
+       /**
+        * The "symlinks" key
+        *
+        * @since 3.3
+        */
+       public static final String CONFIG_KEY_SYMLINKS = "symlinks";
+
        /** The "streamFileThreshold" key */
        public static final String CONFIG_KEY_STREAM_FILE_TRESHOLD = "streamFileThreshold";
 
index 0fc3d4ad0e2904de16e379e952c132912e6afd63..18adb9aa1316aede2fa0dbe58761f6a609e0638f 100644 (file)
@@ -101,6 +101,18 @@ public class CoreConfig {
 
        private final String excludesfile;
 
+       /**
+        * Options for symlink handling
+        *
+        * @since 3.3
+        */
+       public static enum SymLinks {
+               /** Checkout symbolic links as plain files */
+               FALSE,
+               /** Checkout symbolic links as links */
+               TRUE
+       }
+
        private CoreConfig(final Config rc) {
                compression = rc.getInt(ConfigConstants.CONFIG_CORE_SECTION,
                                ConfigConstants.CONFIG_KEY_COMPRESSION, DEFAULT_COMPRESSION);
index eca2f91befddbf3c0f206d86054d4510ed210d9e..414746dc423d7e5ce9f8c9f911c819350339fb82 100644 (file)
@@ -86,6 +86,7 @@ import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
 
 /**
@@ -255,11 +256,11 @@ public class ResolveMerger extends ThreeWayMerger {
        }
 
        private void createDir(File f) throws IOException {
-               if (!f.isDirectory() && !f.mkdirs()) {
+               if (!db.getFS().isDirectory(f) && !f.mkdirs()) {
                        File p = f;
-                       while (p != null && !p.exists())
+                       while (p != null && !db.getFS().exists(p))
                                p = p.getParentFile();
-                       if (p == null || p.isDirectory())
+                       if (p == null || db.getFS().isDirectory(p))
                                throw new IOException(JGitText.get().cannotCreateDirectory);
                        FileUtils.delete(p);
                        if (!f.mkdirs())
@@ -719,9 +720,10 @@ public class ResolveMerger extends ThreeWayMerger {
                                // support write operations
                                throw new UnsupportedOperationException();
 
+                       FS fs = db.getFS();
                        of = new File(workTree, tw.getPathString());
                        File parentFolder = of.getParentFile();
-                       if (!parentFolder.exists())
+                       if (!fs.exists(parentFolder))
                                parentFolder.mkdirs();
                        fos = new FileOutputStream(of);
                        try {
index ce8a50c87f47188a150e0fede9d3ced48e613ef8..cb919ecbcde1f39aa2ce7e4e26b901b945d7d0c5 100644 (file)
@@ -46,6 +46,7 @@
 
 package org.eclipse.jgit.treewalk;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -156,6 +157,8 @@ public class FileTreeIterator extends WorkingTreeIterator {
 
                private long lastModified;
 
+               private FS fs;
+
                /**
                 * Create a new file entry.
                 *
@@ -166,16 +169,26 @@ public class FileTreeIterator extends WorkingTreeIterator {
                 */
                public FileEntry(final File f, FS fs) {
                        file = f;
+                       this.fs = fs;
 
-                       if (f.isDirectory()) {
-                               if (fs.exists(new File(f, Constants.DOT_GIT)))
-                                       mode = FileMode.GITLINK;
+                       @SuppressWarnings("hiding")
+                       FileMode mode = null;
+                       try {
+                               if (fs.isSymLink(f)) {
+                                       mode = FileMode.SYMLINK;
+                               } else if (fs.isDirectory(f)) {
+                                       if (fs.exists(new File(f, Constants.DOT_GIT)))
+                                               mode = FileMode.GITLINK;
+                                       else
+                                               mode = FileMode.TREE;
+                               } else if (fs.canExecute(file))
+                                       mode = FileMode.EXECUTABLE_FILE;
                                else
-                                       mode = FileMode.TREE;
-                       } else if (fs.canExecute(file))
-                               mode = FileMode.EXECUTABLE_FILE;
-                       else
-                               mode = FileMode.REGULAR_FILE;
+                                       mode = FileMode.REGULAR_FILE;
+                       } catch (IOException e) {
+                               mode = FileMode.MISSING;
+                       }
+                       this.mode = mode;
                }
 
                @Override
@@ -190,21 +203,35 @@ public class FileTreeIterator extends WorkingTreeIterator {
 
                @Override
                public long getLength() {
-                       if (length < 0)
-                               length = file.length();
+                       if (length < 0) {
+                               try {
+                                       length = fs.length(file);
+                               } catch (IOException e) {
+                                       length = 0;
+                               }
+                       }
                        return length;
                }
 
                @Override
                public long getLastModified() {
-                       if (lastModified == 0)
-                               lastModified = file.lastModified();
+                       if (lastModified == 0) {
+                               try {
+                                       lastModified = fs.lastModified(file);
+                               } catch (IOException e) {
+                                       lastModified = 0;
+                               }
+                       }
                        return lastModified;
                }
 
                @Override
                public InputStream openInputStream() throws IOException {
-                       return new FileInputStream(file);
+                       if (fs.isSymLink(file))
+                               return new ByteArrayInputStream(fs.readSymLink(file).getBytes(
+                                               Constants.CHARACTER_ENCODING));
+                       else
+                               return new FileInputStream(file);
                }
 
                /**
index 75328c81c620f60834b35c47e8593ec8304e7dad..ac5198cd694b9c1778c9acf4adba08236ef70ba2 100644 (file)
@@ -74,6 +74,7 @@ import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig;
 import org.eclipse.jgit.lib.CoreConfig.CheckStat;
+import org.eclipse.jgit.lib.CoreConfig.SymLinks;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
@@ -202,6 +203,15 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
                ignoreNode = new RootIgnoreNode(entry, repo);
        }
 
+       /**
+        * @return the repository this iterator works with
+        *
+        * @since 3.3
+        */
+       public Repository getRepository() {
+               return repository;
+       }
+
        /**
         * Define the matching {@link DirCacheIterator}, to optimize ObjectIds.
         *
@@ -252,14 +262,10 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
                        }
                }
                switch (mode & FileMode.TYPE_MASK) {
+               case FileMode.TYPE_SYMLINK:
                case FileMode.TYPE_FILE:
                        contentIdFromPtr = ptr;
                        return contentId = idBufferBlob(entries[ptr]);
-               case FileMode.TYPE_SYMLINK:
-                       // Java does not support symbolic links, so we should not
-                       // have reached this particular part of the walk code.
-                       //
-                       return zeroid;
                case FileMode.TYPE_GITLINK:
                        contentIdFromPtr = ptr;
                        return contentId = idSubmodule(entries[ptr]);
@@ -723,8 +729,9 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
                        return false;
 
                // Do not rely on filemode differences in case of symbolic links
-               if (FileMode.SYMLINK.equals(rawMode))
-                       return false;
+               if (getOptions().getSymLinks() == SymLinks.FALSE)
+                       if (FileMode.SYMLINK.equals(rawMode))
+                               return false;
 
                // Ignore the executable file bits if WorkingTreeOptions tell me to
                // do so. Ignoring is done by setting the bits representing a
index 15bfb917db718958a2c2485178e48089b9a20fb6..e5e22413d233b094ec3a03fba2b3ba6aa39cb529 100644 (file)
@@ -48,6 +48,7 @@ import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Config.SectionParser;
 import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
 import org.eclipse.jgit.lib.CoreConfig.CheckStat;
+import org.eclipse.jgit.lib.CoreConfig.SymLinks;
 
 /** Options used by the {@link WorkingTreeIterator}. */
 public class WorkingTreeOptions {
@@ -64,6 +65,8 @@ public class WorkingTreeOptions {
 
        private final CheckStat checkStat;
 
+       private final SymLinks symlinks;
+
        private WorkingTreeOptions(final Config rc) {
                fileMode = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
                                ConfigConstants.CONFIG_KEY_FILEMODE, true);
@@ -71,6 +74,8 @@ public class WorkingTreeOptions {
                                ConfigConstants.CONFIG_KEY_AUTOCRLF, AutoCRLF.FALSE);
                checkStat = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
                                ConfigConstants.CONFIG_KEY_CHECKSTAT, CheckStat.DEFAULT);
+               symlinks = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_SYMLINKS, SymLinks.TRUE);
        }
 
        /** @return true if the execute bit on working files should be trusted. */
@@ -90,4 +95,12 @@ public class WorkingTreeOptions {
        public CheckStat getCheckStat() {
                return checkStat;
        }
+
+       /**
+        * @return how we handle symbolic links
+        * @since 3.3
+        */
+       public SymLinks getSymLinks() {
+               return symlinks;
+       }
 }