From 35f55ae7e034275811fa68908215b48bbf9df965 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 17 Sep 2013 17:05:00 -0400
Subject: [PATCH] Merge branch 'init-shared' of https://github.com/fzs/gitblit into prefixes

---
 .classpath                                         |    2 
 src/main/java/com/gitblit/utils/JGitUtils.java     |  182 +++++++++++
 src/test/java/com/gitblit/tests/JGitUtilsTest.java |  134 ++++++++
 src/test/java/com/gitblit/tests/JnaUtilsTest.java  |  152 ++++++++++
 src/main/distrib/data/gitblit.properties           |    9 
 src/main/java/com/gitblit/GitBlit.java             |   11 
 build.moxie                                        |    1 
 gitblit.iml                                        |   22 
 src/main/java/com/gitblit/utils/JnaUtils.java      |  364 ++++++++++++++++++++++++
 9 files changed, 860 insertions(+), 17 deletions(-)

diff --git a/.classpath b/.classpath
index 04b4bba..ad8756c 100644
--- a/.classpath
+++ b/.classpath
@@ -37,6 +37,7 @@
 	<classpathentry kind="lib" path="ext/jcalendar-1.3.2.jar" />
 	<classpathentry kind="lib" path="ext/commons-compress-1.4.1.jar" sourcepath="ext/src/commons-compress-1.4.1.jar" />
 	<classpathentry kind="lib" path="ext/xz-1.0.jar" sourcepath="ext/src/xz-1.0.jar" />
+	<classpathentry kind="lib" path="ext/commons-io-2.2.jar" sourcepath="ext/src/commons-io-2.2.jar" />
 	<classpathentry kind="lib" path="ext/force-partner-api-24.0.0.jar" sourcepath="ext/src/force-partner-api-24.0.0.jar" />
 	<classpathentry kind="lib" path="ext/force-wsc-24.0.0.jar" sourcepath="ext/src/force-wsc-24.0.0.jar" />
 	<classpathentry kind="lib" path="ext/js-1.7R2.jar" sourcepath="ext/src/js-1.7R2.jar" />
@@ -60,7 +61,6 @@
 	<classpathentry kind="lib" path="ext/httpcore-4.2.1.jar" sourcepath="ext/src/httpcore-4.2.1.jar" />
 	<classpathentry kind="lib" path="ext/commons-logging-1.1.1.jar" sourcepath="ext/src/commons-logging-1.1.1.jar" />
 	<classpathentry kind="lib" path="ext/commons-exec-1.1.jar" sourcepath="ext/src/commons-exec-1.1.jar" />
-	<classpathentry kind="lib" path="ext/commons-io-2.2.jar" sourcepath="ext/src/commons-io-2.2.jar" />
 	<classpathentry kind="output" path="bin/classes" />
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" />
 </classpath>
\ No newline at end of file
diff --git a/build.moxie b/build.moxie
index fe4eeb8..4a3aa34 100644
--- a/build.moxie
+++ b/build.moxie
@@ -148,6 +148,7 @@
 - compile 'org.apache.ivy:ivy:2.2.0' :war
 - compile 'com.toedter:jcalendar:1.3.2' :authority
 - compile 'org.apache.commons:commons-compress:1.4.1' :war
+- compile 'commons-io:commons-io:2.2' :war
 - compile 'com.force.api:force-partner-api:24.0.0' :war
 - compile 'org.freemarker:freemarker:2.3.19' :war
 - compile 'com.github.dblock.waffle:waffle-jna:1.5' :war
diff --git a/gitblit.iml b/gitblit.iml
index d29b31a..82d37a9 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -381,6 +381,17 @@
       </library>
     </orderEntry>
     <orderEntry type="module-library">
+      <library name="commons-io-2.2.jar">
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/ext/commons-io-2.2.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES>
+          <root url="jar://$MODULE_DIR$/ext/src/commons-io-2.2.jar!/" />
+        </SOURCES>
+      </library>
+    </orderEntry>
+    <orderEntry type="module-library">
       <library name="force-partner-api-24.0.0.jar">
         <CLASSES>
           <root url="jar://$MODULE_DIR$/ext/force-partner-api-24.0.0.jar!/" />
@@ -630,17 +641,6 @@
         <JAVADOC />
         <SOURCES>
           <root url="jar://$MODULE_DIR$/ext/src/commons-exec-1.1.jar!/" />
-        </SOURCES>
-      </library>
-    </orderEntry>
-    <orderEntry type="module-library" scope="TEST">
-      <library name="commons-io-2.2.jar">
-        <CLASSES>
-          <root url="jar://$MODULE_DIR$/ext/commons-io-2.2.jar!/" />
-        </CLASSES>
-        <JAVADOC />
-        <SOURCES>
-          <root url="jar://$MODULE_DIR$/ext/src/commons-io-2.2.jar!/" />
         </SOURCES>
       </library>
     </orderEntry>
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index 665a90e..e4e7153 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -191,6 +191,15 @@
 # SINCE 1.3.0
 git.defaultIncrementalPushTagPrefix = r
 
+# In an Unix environment where mixed access methods exist for shared repositories,
+# the repository should be created with 'git init --shared' to make sure that
+# it can be accessed e.g. via ssh (user git) and http (user www-data).
+# Valid values are the values available for the '--shared' option. The the manual
+# page for 'git init' for more information on shared repositories.
+#
+# SINCE 1.3.2
+git.createRepositoriesShared = false
+
 # Enable JGit-based garbage collection. (!!EXPERIMENTAL!!)
 #
 # USE AT YOUR OWN RISK!
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 72a0e22..1afbbc9 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -2427,7 +2427,8 @@
 			}
 			// create repository
 			logger.info("create repository " + repository.name);
-			r = JGitUtils.createRepository(repositoriesFolder, repository.name);
+			String shared = getString(Keys.git.createRepositoriesShared, "FALSE");
+			r = JGitUtils.createRepository(repositoriesFolder, repository.name, shared);
 		} else {
 			// rename repository
 			if (!repositoryName.equalsIgnoreCase(repository.name)) {
@@ -2529,7 +2530,13 @@
 			// close the repository object
 			r.close();
 		}
-		
+
+		// Adjust permissions in case we updated the config files
+		JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "config"),
+				getString(Keys.git.createRepositoriesShared, "FALSE"));
+		JGitUtils.adjustSharedPerm(new File(r.getDirectory().getAbsolutePath(), "HEAD"),
+				getString(Keys.git.createRepositoriesShared, "FALSE"));
+
 		// update repository cache
 		removeFromCachedRepositoryList(repositoryName);
 		// model will actually be replaced on next load because config is stale
diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java
index 3f01eea..cf6ec26 100644
--- a/src/main/java/com/gitblit/utils/JGitUtils.java
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -32,6 +32,7 @@
 import java.util.Map.Entry;
 import java.util.regex.Pattern;
 
+import org.apache.commons.io.filefilter.TrueFileFilter;
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.FetchCommand;
 import org.eclipse.jgit.api.Git;
@@ -58,6 +59,7 @@
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.lib.TreeFormatter;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -259,14 +261,188 @@
 	 * @return Repository
 	 */
 	public static Repository createRepository(File repositoriesFolder, String name) {
+		return createRepository(repositoriesFolder, name, "FALSE");
+	}
+
+	/**
+	 * Creates a bare, shared repository.
+	 *
+	 * @param repositoriesFolder
+	 * @param name
+	 * @param shared
+	 *          the setting for the --shared option of "git init".
+	 * @return Repository
+	 */
+	public static Repository createRepository(File repositoriesFolder, String name, String shared) {
 		try {
-			Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call();
-			return git.getRepository();
-		} catch (GitAPIException e) {
+			Repository repo = null;
+			try {
+				Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call();
+				repo = git.getRepository();
+			} catch (GitAPIException e) {
+				throw new RuntimeException(e);
+			}
+
+			GitConfigSharedRepository sharedRepository = new GitConfigSharedRepository(shared);
+			if (sharedRepository.isShared()) {
+				StoredConfig config = repo.getConfig();
+				config.setString("core", null, "sharedRepository", sharedRepository.getValue());
+				config.setBoolean("receive", null, "denyNonFastforwards", true);
+				config.save();
+
+				if (! JnaUtils.isWindows()) {
+					Iterator<File> iter = org.apache.commons.io.FileUtils.iterateFilesAndDirs(repo.getDirectory(),
+							TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
+					// Adjust permissions on file/directory
+					while (iter.hasNext()) {
+						adjustSharedPerm(iter.next(), sharedRepository);
+					}
+				}
+			}
+
+			return repo;
+		} catch (IOException e) {
 			throw new RuntimeException(e);
 		}
 	}
 
+	private enum GitConfigSharedRepositoryValue
+	{
+		UMASK("0", 0), FALSE("0", 0), OFF("0", 0), NO("0", 0),
+		GROUP("1", 0660), TRUE("1", 0660), ON("1", 0660), YES("1", 0660),
+		ALL("2", 0664), WORLD("2", 0664), EVERYBODY("2", 0664),
+		Oxxx(null, -1);
+
+		private String configValue;
+		private int permValue;
+		private GitConfigSharedRepositoryValue(String config, int perm) { configValue = config; permValue = perm; };
+
+		public String getConfigValue() { return configValue; };
+		public int getPerm() { return permValue; };
+
+	}
+
+	private static class GitConfigSharedRepository
+	{
+		private int intValue;
+		private GitConfigSharedRepositoryValue enumValue;
+
+		GitConfigSharedRepository(String s) {
+			if ( s == null || s.trim().isEmpty() ) {
+				enumValue = GitConfigSharedRepositoryValue.GROUP;
+			}
+			else {
+				try {
+					// Try one of the string values
+					enumValue = GitConfigSharedRepositoryValue.valueOf(s.trim().toUpperCase());
+				} catch (IllegalArgumentException  iae) {
+					try {
+						// Try if this is an octal number
+						int i = Integer.parseInt(s, 8);
+						if ( (i & 0600) != 0600 ) {
+							String msg = String.format("Problem with core.sharedRepository filemode value (0%03o).\nThe owner of files must always have read and write permissions.", i);
+							throw new IllegalArgumentException(msg);
+						}
+						intValue = i & 0666;
+						enumValue = GitConfigSharedRepositoryValue.Oxxx;
+					} catch (NumberFormatException nfe) {
+						throw new IllegalArgumentException("Bad configuration value for 'shared': '" + s + "'");
+					}
+				}
+			}
+		}
+
+		String getValue() {
+			if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) {
+				if (intValue == 0) return "0";
+				return String.format("0%o", intValue);
+			}
+			return enumValue.getConfigValue();
+		}
+
+		int getPerm() {
+			if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) return intValue;
+			return enumValue.getPerm();
+		}
+
+		boolean isCustom() {
+			return enumValue == GitConfigSharedRepositoryValue.Oxxx;
+		}
+
+		boolean isShared() {
+			return (enumValue.getPerm() > 0) || enumValue == GitConfigSharedRepositoryValue.Oxxx;
+		}
+	}
+
+
+	/**
+	 * Adjust file permissions of a file/directory for shared repositories
+	 *
+	 * @param path
+	 * 			File that should get its permissions changed.
+	 * @param configShared
+	 * 			Configuration string value for the shared mode.
+	 * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned.
+	 */
+	public static int adjustSharedPerm(File path, String configShared) {
+		return adjustSharedPerm(path, new GitConfigSharedRepository(configShared));
+	}
+
+
+	/**
+	 * Adjust file permissions of a file/directory for shared repositories
+	 *
+	 * @param path
+	 * 			File that should get its permissions changed.
+	 * @param configShared
+	 * 			Configuration setting for the shared mode.
+	 * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned.
+	 */
+	public static int adjustSharedPerm(File path, GitConfigSharedRepository configShared) {
+		if (! configShared.isShared()) return 0;
+		if (! path.exists()) return -1;
+
+		int perm = configShared.getPerm();
+		JnaUtils.Filestat stat = JnaUtils.getFilestat(path);
+		if (stat == null) return -1;
+		int mode = stat.mode;
+		if (mode < 0) return -1;
+
+		// Now, here is the kicker: Under Linux, chmod'ing a sgid file whose guid is different from the process'
+		// effective guid will reset the sgid flag of the file. Since there is no way to get the sgid flag back in
+		// that case, we decide to rather not touch is and getting the right permissions will have to be achieved
+		// in a different way, e.g. by using an appropriate umask for the Gitblit process.
+		if (System.getProperty("os.name").toLowerCase().startsWith("linux")) {
+			if ( ((mode & (JnaUtils.S_ISGID | JnaUtils.S_ISUID)) != 0)
+				&& stat.gid != JnaUtils.getegid() ) {
+				LOGGER.debug("Not adjusting permissions to prevent clearing suid/sgid bits for '" + path + "'" );
+				return 0;
+			}
+		}
+
+		// If the owner has no write access, delete it from group and other, too.
+		if ((mode & JnaUtils.S_IWUSR) == 0) perm &= ~0222;
+		// If the owner has execute access, set it for all blocks that have read access.
+		if ((mode & JnaUtils.S_IXUSR) == JnaUtils.S_IXUSR) perm |= (perm & 0444) >> 2;
+
+		if (configShared.isCustom()) {
+			// Use the custom value for access permissions.
+			mode = (mode & ~0777) | perm;
+		}
+		else {
+			// Just add necessary bits to existing permissions.
+			mode |= perm;
+		}
+
+		if (path.isDirectory()) {
+			mode |= (mode & 0444) >> 2;
+			mode |= JnaUtils.S_ISGID;
+		}
+
+		return JnaUtils.setFilemode(path, mode);
+	}
+
+
 	/**
 	 * Returns a list of repository names in the specified folder.
 	 * 
diff --git a/src/main/java/com/gitblit/utils/JnaUtils.java b/src/main/java/com/gitblit/utils/JnaUtils.java
new file mode 100644
index 0000000..4009342
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/JnaUtils.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Collection of static methods to access native OS library functionality.
+ *
+ * @author Florian Zschocke
+ */
+public class JnaUtils {
+	public static final int S_ISUID =  0004000; // set user id on execution
+	public static final int S_ISGID =  0002000; // set group id on execution
+	public static final int S_ISVTX =  0001000; // sticky bit, save swapped text even after use
+
+	public static final int S_IRWXU =  0000700; // RWX mask for owner
+	public static final int S_IRUSR =  0000400; // read permission for owner
+	public static final int S_IWUSR =  0000200; // write permission for owner
+	public static final int S_IXUSR =  0000100; // execute/search permission for owner
+	public static final int S_IRWXG =  0000070; // RWX mask for group
+	public static final int S_IRGRP =  0000040; // read permission for group
+	public static final int S_IWGRP =  0000020; // write permission for group
+	public static final int S_IXGRP =  0000010; // execute/search permission for group
+	public static final int S_IRWXO =  0000007; // RWX mask for other
+	public static final int S_IROTH =  0000004; // read permission for other
+	public static final int S_IWOTH =  0000002; // write permission for other
+	public static final int S_IXOTH =  0000001; // execute/search permission for other
+
+	public static final int S_IFMT =   0170000; // type of file mask
+	public static final int S_IFIFO =  0010000; // named pipe (fifo)
+	public static final int S_IFCHR =  0020000; // character special device
+	public static final int S_IFDIR =  0040000; // directory
+	public static final int S_IFBLK =  0060000; // block special device
+	public static final int S_IFREG =  0100000; // regular file
+	public static final int S_IFLNK =  0120000; // symbolic link
+	public static final int S_IFSOCK = 0140000; // socket
+
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
+
+	private static UnixCLibrary unixlibc = null;
+
+
+	/**
+	 * Utility method to check if the JVM is running on a Windows OS.
+	 *
+	 * @return true, if the system property 'os.name' starts with 'Windows'.
+	 */
+	public static boolean isWindows()
+	{
+		return System.getProperty("os.name").toLowerCase().startsWith("windows");
+	}
+
+
+	private interface UnixCLibrary extends Library {
+		public int chmod(String path, int mode);
+		public int getgid();
+		public int getegid();
+	}
+
+
+	public static int getgid()
+	{
+		if (isWindows()) {
+			throw new UnsupportedOperationException("The method JnaUtils.getgid is not supported under Windows.");
+		}
+
+		return getUnixCLibrary().getgid();
+	}
+
+
+	public static int getegid()
+	{
+		if (isWindows()) {
+			throw new UnsupportedOperationException("The method JnaUtils.getegid is not supported under Windows.");
+		}
+
+		return getUnixCLibrary().getegid();
+	}
+
+
+	/**
+	 * Set the permission bits of a file.
+	 *
+	 * The permission bits are set to the provided mode. This method is only
+	 * implemented for OSes of the Unix family and makes use of the 'chmod'
+	 * function of the native C library. See 'man 2 chmod' for more information.
+	 *
+	 * @param path
+	 * 			File/directory to set the permission bits for.
+	 * @param mode
+	 * 			A mode created from or'd permission bit masks S_I*
+	 * @return	Upon successful completion, a value of 0 returned. Otherwise, a value of -1 is returned.
+	 */
+	public static int setFilemode(File file, int mode)
+	{
+		return setFilemode(file.getAbsolutePath(), mode);
+	}
+
+	/**
+	 * Set the permission bits of a file.
+	 *
+	 * The permission bits are set to the provided mode. This method is only
+	 * implemented for OSes of the Unix family and makes use of the 'chmod'
+	 * function of the native C library. See 'man 2 chmod' for more information.
+	 *
+	 * @param path
+	 * 			Path to a file/directory to set the permission bits for.
+	 * @param mode
+	 * 			A mode created from or'd permission bit masks S_I*
+	 * @return	Upon successful completion, a value of 0 returned. Otherwise, a value of -1 is returned.
+	 */
+	public static int setFilemode(String path, int mode)
+	{
+		if (isWindows()) {
+			throw new UnsupportedOperationException("The method JnaUtils.getFilemode is not supported under Windows.");
+		}
+
+		return getUnixCLibrary().chmod(path, mode);
+	}
+
+
+
+	/**
+	 * Get the file mode bits of a file.
+	 *
+	 * This method is only implemented for OSes of the Unix family. It returns the file mode
+	 * information as available in the st_mode member of the resulting struct stat when calling
+	 * 'lstat' on a file.
+	 *
+	 * @param path
+	 * 			File/directory to get the file mode from.
+	 * @return	Upon successful completion, the file mode bits are returned. Otherwise, a value of -1 is returned.
+	 */
+	public static int getFilemode(File path)
+	{
+		return getFilemode(path.getAbsolutePath());
+	}
+
+	/**
+	 * Get the file mode bits of a file.
+	 *
+	 * This method is only implemented for OSes of the Unix family. It returns the file mode
+	 * information as available in the st_mode member of the resulting struct stat when calling
+	 * 'lstat' on a file.
+	 *
+	 * @param path
+	 * 			Path to a file/directory to get the file mode from.
+	 * @return	Upon successful completion, the file mode bits are returned. Otherwise, a value of -1 is returned.
+	 */
+	public static int getFilemode(String path)
+	{
+		if (isWindows()) {
+			throw new UnsupportedOperationException("The method JnaUtils.getFilemode is not supported under Windows.");
+		}
+
+		Filestat stat = getFilestat(path);
+		if ( stat == null ) return -1;
+		return stat.mode;
+	}
+
+
+	/**
+	 * Status information of a file.
+	 */
+	public static class Filestat
+	{
+		public int mode;  // file mode, permissions, type
+		public int uid;   // user Id of owner
+		public int gid;   // group Id of owner
+
+		Filestat(int mode, int uid, int gid) {
+			this.mode = mode; this.uid = uid; this.gid = gid;
+		}
+	}
+
+
+	/**
+	 * Get Unix file status information for a file.
+	 *
+	 * This method is only implemented for OSes of the Unix family. It returns file status
+	 * information for a file. Currently this is the file mode, the user id and group id of the owner.
+	 *
+	 * @param path
+	 * 			File/directory to get the file status from.
+	 * @return	Upon successful completion, a Filestat object containing the file information is returned.
+	 * 			Otherwise, null is returned.
+	 */
+	public static Filestat getFilestat(File path)
+	{
+		return getFilestat(path.getAbsolutePath());
+	}
+
+
+	/**
+	 * Get Unix file status information for a file.
+	 *
+	 * This method is only implemented for OSes of the Unix family. It returns file status
+	 * information for a file. Currently this is the file mode, the user id and group id of the owner.
+	 *
+	 * @param path
+	 * 			Path to a file/directory to get the file status from.
+	 * @return	Upon successful completion, a Filestat object containing the file information is returned.
+	 * 			Otherwise, null is returned.
+	 */
+	public static Filestat getFilestat(String path)
+	{
+		if (isWindows()) {
+			throw new UnsupportedOperationException("The method JnaUtils.getFilestat is not supported under Windows.");
+		}
+
+
+		int mode = 0;
+
+		// Use a Runtime, because implementing stat() via JNA is just too much trouble.
+		// This could be done with the 'stat' command, too. But that may have a shell specific implementation, so we use 'ls' instead.
+		String lsLine = runProcessLs(path);
+		if (lsLine == null) {
+			LOGGER.debug("Could not get file information for path " + path);
+			return null;
+		}
+
+		Pattern p = Pattern.compile("^(([-bcdlspCDMnP?])([-r][-w][-xSs])([-r][-w][-xSs])([-r][-w][-xTt]))[@+.]? +[0-9]+ +([0-9]+) +([0-9]+) ");
+		Matcher m = p.matcher(lsLine);
+		if ( !m.lookingAt() ) {
+			LOGGER.debug("Could not parse valid file mode information for path " + path);
+			return null;
+		}
+
+		// Parse mode string to mode bits
+		String group = m.group(2);
+		switch (group.charAt(0)) {
+		case 'p' :
+			mode |= 0010000; break;
+		case 'c':
+			mode |= 0020000; break;
+		case 'd':
+			mode |= 0040000; break;
+		case 'b':
+			mode |= 0060000; break;
+		case '-':
+			mode |= 0100000; break;
+		case 'l':
+			mode |= 0120000; break;
+		case 's':
+			mode |= 0140000; break;
+		}
+
+		for ( int i = 0; i < 3; i++) {
+			group = m.group(3 + i);
+			switch (group.charAt(0)) {
+			case 'r':
+				mode |= (0400 >> i*3); break;
+			case '-':
+				break;
+			}
+
+			switch (group.charAt(1)) {
+			case 'w':
+				mode |= (0200 >> i*3); break;
+			case '-':
+				break;
+			}
+
+			switch (group.charAt(2)) {
+			case 'x':
+				mode |= (0100 >> i*3); break;
+			case 'S':
+				mode |= (04000 >> i); break;
+			case 's':
+				mode |= (0100 >> i*3);
+				mode |= (04000 >> i); break;
+			case 'T':
+				mode |= 01000; break;
+			case 't':
+				mode |= (0100 >> i*3);
+				mode |= 01000; break;
+			case '-':
+				break;
+			}
+		}
+
+		return new Filestat(mode, Integer.parseInt(m.group(6)), Integer.parseInt(m.group(7)));
+	}
+
+
+	/**
+	 * Run the unix command 'ls -ldn' on a single file and return the resulting output line.
+	 *
+	 * @param path
+	 * 			Path to a single file or directory.
+	 * @return The first line of output from the 'ls' command. Null, if an error occurred and no line could be read.
+	 */
+	private static String runProcessLs(String path)
+	{
+		String cmd = "ls -ldn " + path;
+		Runtime rt = Runtime.getRuntime();
+		Process pr = null;
+		InputStreamReader ir = null;
+		BufferedReader br = null;
+		String output = null;
+
+		try {
+			pr = rt.exec(cmd);
+			ir = new InputStreamReader(pr.getInputStream());
+			br = new BufferedReader(ir);
+
+			output = br.readLine();
+
+			while (br.readLine() != null) ; // Swallow remaining output
+		}
+		catch (IOException e) {
+			LOGGER.debug("Exception while running unix command '" + cmd + "': " + e);
+		}
+		finally {
+			if (pr != null) try { pr.waitFor();	} catch (Exception ignored) {}
+
+			if (br != null) try { br.close(); } catch (Exception ignored) {}
+			if (ir != null) try { ir.close(); } catch (Exception ignored) {}
+
+			if (pr != null) try { pr.getOutputStream().close();	} catch (Exception ignored) {}
+			if (pr != null) try { pr.getInputStream().close();	} catch (Exception ignored) {}
+			if (pr != null) try { pr.getErrorStream().close();	} catch (Exception ignored) {}
+		}
+
+		return output;
+	}
+
+
+	private static UnixCLibrary getUnixCLibrary()
+	{
+		if (unixlibc == null) {
+			unixlibc = (UnixCLibrary) Native.loadLibrary("c", UnixCLibrary.class);
+			if (unixlibc == null) throw new RuntimeException("Could not initialize native C library.");
+		}
+		return unixlibc;
+	}
+
+}
diff --git a/src/test/java/com/gitblit/tests/JGitUtilsTest.java b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
index 375dbd5..463c0a8 100644
--- a/src/test/java/com/gitblit/tests/JGitUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
@@ -52,6 +52,7 @@
 import com.gitblit.models.RefModel;
 import com.gitblit.utils.CompressionUtils;
 import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.JnaUtils;
 import com.gitblit.utils.StringUtils;
 
 public class JGitUtilsTest {
@@ -149,6 +150,139 @@
 	}
 
 	@Test
+	public void testCreateRepositoryShared() throws Exception {
+		String[] repositories = { "NewSharedTestRepository.git" };
+		for (String repositoryName : repositories) {
+			Repository repository = JGitUtils.createRepository(GitBlitSuite.REPOSITORIES,
+					repositoryName, "group");
+			File folder = FileKey.resolve(new File(GitBlitSuite.REPOSITORIES, repositoryName),
+					FS.DETECTED);
+			assertNotNull(repository);
+			assertFalse(JGitUtils.hasCommits(repository));
+			assertNull(JGitUtils.getFirstCommit(repository, null));
+
+			assertEquals("1", repository.getConfig().getString("core", null, "sharedRepository"));
+
+			assertTrue(folder.exists());
+			if (! JnaUtils.isWindows()) {
+				int mode = JnaUtils.getFilemode(folder);
+				assertEquals(JnaUtils.S_ISGID, mode & JnaUtils.S_ISGID);
+				assertEquals(JnaUtils.S_IRWXG, mode & JnaUtils.S_IRWXG);
+
+				mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/HEAD");
+				assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
+
+				mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/config");
+				assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
+			}
+
+			repository.close();
+			RepositoryCache.close(repository);
+			FileUtils.delete(repository.getDirectory(), FileUtils.RECURSIVE);
+		}
+	}
+
+	@Test
+	public void testCreateRepositorySharedCustom() throws Exception {
+		String[] repositories = { "NewSharedTestRepository.git" };
+		for (String repositoryName : repositories) {
+			Repository repository = JGitUtils.createRepository(GitBlitSuite.REPOSITORIES,
+					repositoryName, "660");
+			File folder = FileKey.resolve(new File(GitBlitSuite.REPOSITORIES, repositoryName),
+					FS.DETECTED);
+			assertNotNull(repository);
+			assertFalse(JGitUtils.hasCommits(repository));
+			assertNull(JGitUtils.getFirstCommit(repository, null));
+
+			assertEquals("0660", repository.getConfig().getString("core", null, "sharedRepository"));
+
+			assertTrue(folder.exists());
+			if (! JnaUtils.isWindows()) {
+				int mode = JnaUtils.getFilemode(folder);
+				assertEquals(JnaUtils.S_ISGID, mode & JnaUtils.S_ISGID);
+				assertEquals(JnaUtils.S_IRWXG, mode & JnaUtils.S_IRWXG);
+				assertEquals(0, mode & JnaUtils.S_IRWXO);
+
+				mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/HEAD");
+				assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
+				assertEquals(0, mode & JnaUtils.S_IRWXO);
+
+				mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/config");
+				assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
+				assertEquals(0, mode & JnaUtils.S_IRWXO);
+			}
+
+			repository.close();
+			RepositoryCache.close(repository);
+			FileUtils.delete(repository.getDirectory(), FileUtils.RECURSIVE);
+		}
+	}
+
+	@Test
+	public void testCreateRepositorySharedSgidParent() throws Exception {
+		if (! JnaUtils.isWindows()) {
+			String repositoryAll = "NewTestRepositoryAll.git";
+			String repositoryUmask = "NewTestRepositoryUmask.git";
+			String sgidParent = "sgid";
+			
+			File parent = new File(GitBlitSuite.REPOSITORIES, sgidParent);
+			File folder = null;
+			boolean parentExisted = parent.exists();
+			try {
+				if (!parentExisted) {
+					assertTrue("Could not create SGID parent folder.", parent.mkdir());
+				}
+				int mode = JnaUtils.getFilemode(parent);
+				assertTrue(mode > 0);
+				assertEquals(0, JnaUtils.setFilemode(parent, mode | JnaUtils.S_ISGID | JnaUtils.S_IWGRP));
+
+				Repository repository = JGitUtils.createRepository(parent, repositoryAll, "all");
+				folder = FileKey.resolve(new File(parent, repositoryAll), FS.DETECTED);
+				assertNotNull(repository);
+		
+				assertEquals("2", repository.getConfig().getString("core", null, "sharedRepository"));
+		
+				assertTrue(folder.exists());
+				mode = JnaUtils.getFilemode(folder);
+				assertEquals(JnaUtils.S_ISGID, mode & JnaUtils.S_ISGID);
+	
+				mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/HEAD");
+				assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
+				assertEquals(JnaUtils.S_IROTH, mode & JnaUtils.S_IRWXO);
+	
+				mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/config");
+				assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, mode & JnaUtils.S_IRWXG);
+				assertEquals(JnaUtils.S_IROTH, mode & JnaUtils.S_IRWXO);
+	
+				repository.close();
+				RepositoryCache.close(repository);
+
+
+
+				repository = JGitUtils.createRepository(parent, repositoryUmask, "umask");
+				folder = FileKey.resolve(new File(parent, repositoryUmask), FS.DETECTED);
+				assertNotNull(repository);
+		
+				assertEquals(null, repository.getConfig().getString("core", null, "sharedRepository"));
+		
+				assertTrue(folder.exists());
+				mode = JnaUtils.getFilemode(folder);
+				assertEquals(JnaUtils.S_ISGID, mode & JnaUtils.S_ISGID);
+	
+				repository.close();
+				RepositoryCache.close(repository);
+			}
+			finally {
+				FileUtils.delete(new File(parent, repositoryAll), FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
+				FileUtils.delete(new File(parent, repositoryUmask), FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
+				if (!parentExisted) {
+					FileUtils.delete(parent, FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
+				}
+			}
+		}
+	}
+
+	@Test
 	public void testRefs() throws Exception {
 		Repository repository = GitBlitSuite.getJGitRepository();
 		Map<ObjectId, List<RefModel>> map = JGitUtils.getAllRefs(repository);
diff --git a/src/test/java/com/gitblit/tests/JnaUtilsTest.java b/src/test/java/com/gitblit/tests/JnaUtilsTest.java
new file mode 100644
index 0000000..25d1ccf
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/JnaUtilsTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.tests;
+
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.JnaUtils;
+import java.io.File;
+import java.io.IOException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+import org.junit.Test;
+
+/**
+ *
+ * @author Florian Zschocke
+ */
+public class JnaUtilsTest {
+
+	@Test
+	public void testGetgid() {
+		if (JnaUtils.isWindows()) {
+			try {
+				JnaUtils.getFilemode(GitBlitSuite.REPOSITORIES);
+			} catch(UnsupportedOperationException e) {}
+		}
+		else {
+			int gid = JnaUtils.getgid();
+			assertTrue(gid >= 0);
+			int egid = JnaUtils.getegid();
+			assertTrue(egid >= 0);
+			assertTrue("Really? You're running unit tests as root?!", gid > 0);
+			System.out.println("gid: " + gid + "  egid: " + egid);
+		}
+	}
+
+
+	@Test
+	public void testGetFilemode() throws IOException {
+		if (JnaUtils.isWindows()) {
+			try {
+				JnaUtils.getFilemode(GitBlitSuite.REPOSITORIES);
+			} catch(UnsupportedOperationException e) {}
+		}
+		else {
+			String repositoryName = "NewJnaTestRepository.git";
+			Repository repository = JGitUtils.createRepository(GitBlitSuite.REPOSITORIES, repositoryName);
+			File folder = FileKey.resolve(new File(GitBlitSuite.REPOSITORIES, repositoryName), FS.DETECTED);
+			assertTrue(folder.exists());
+
+			int mode = JnaUtils.getFilemode(folder);
+			assertTrue(mode > 0);
+			assertEquals(JnaUtils.S_IFDIR, (mode & JnaUtils.S_IFMT)); // directory
+			assertEquals(JnaUtils.S_IRUSR | JnaUtils.S_IWUSR | JnaUtils.S_IXUSR, (mode & JnaUtils.S_IRWXU)); // owner full access
+
+			mode = JnaUtils.getFilemode(folder.getAbsolutePath() + "/config");
+			assertTrue(mode > 0);
+			assertEquals(JnaUtils.S_IFREG, (mode & JnaUtils.S_IFMT)); // directory
+			assertEquals(JnaUtils.S_IRUSR | JnaUtils.S_IWUSR, (mode & JnaUtils.S_IRWXU)); // owner full access
+
+			repository.close();
+			RepositoryCache.close(repository);
+			FileUtils.deleteDirectory(repository.getDirectory());
+			}
+	}
+
+
+	@Test
+	public void testSetFilemode() throws IOException {
+		if (JnaUtils.isWindows()) {
+			try {
+				JnaUtils.getFilemode(GitBlitSuite.REPOSITORIES);
+			} catch(UnsupportedOperationException e) {}
+		}
+		else {
+			String repositoryName = "NewJnaTestRepository.git";
+			Repository repository = JGitUtils.createRepository(GitBlitSuite.REPOSITORIES, repositoryName);
+			File folder = FileKey.resolve(new File(GitBlitSuite.REPOSITORIES, repositoryName), FS.DETECTED);
+			assertTrue(folder.exists());
+
+			File path = new File(folder, "refs");
+			int mode = JnaUtils.getFilemode(path);
+			assertTrue(mode > 0);
+			assertEquals(JnaUtils.S_IFDIR, (mode & JnaUtils.S_IFMT)); // directory
+			assertEquals(JnaUtils.S_IRUSR | JnaUtils.S_IWUSR | JnaUtils.S_IXUSR, (mode & JnaUtils.S_IRWXU)); // owner full access
+
+			mode |= JnaUtils.S_ISGID;
+			mode |= JnaUtils.S_IRWXG;
+			int ret = JnaUtils.setFilemode(path, mode);
+			assertEquals(0, ret);
+			mode = JnaUtils.getFilemode(path);
+			assertTrue(mode > 0);
+			assertEquals(JnaUtils.S_ISGID, (mode & JnaUtils.S_ISGID)); // set-gid-bit set
+			assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP | JnaUtils.S_IXGRP, (mode & JnaUtils.S_IRWXG)); // group full access
+
+			path = new File(folder, "config");
+			mode = JnaUtils.getFilemode(path.getAbsolutePath());
+			assertTrue(mode > 0);
+			assertEquals(JnaUtils.S_IFREG, (mode & JnaUtils.S_IFMT)); // directory
+			assertEquals(JnaUtils.S_IRUSR | JnaUtils.S_IWUSR, (mode & JnaUtils.S_IRWXU)); // owner full access
+
+			mode |= (JnaUtils.S_IRGRP | JnaUtils.S_IWGRP);
+			ret = JnaUtils.setFilemode(path.getAbsolutePath(), mode);
+			assertEquals(0, ret);
+			mode = JnaUtils.getFilemode(path.getAbsolutePath());
+			assertTrue(mode > 0);
+			assertEquals(JnaUtils.S_IRGRP | JnaUtils.S_IWGRP, (mode & JnaUtils.S_IRWXG)); // group full access
+
+			repository.close();
+			RepositoryCache.close(repository);
+			FileUtils.deleteDirectory(repository.getDirectory());
+		}
+	}
+
+
+	@Test
+	public void testGetFilestat() {
+		if (JnaUtils.isWindows()) {
+			try {
+				JnaUtils.getFilemode(GitBlitSuite.REPOSITORIES);
+			} catch(UnsupportedOperationException e) {}
+		}
+		else {
+			JnaUtils.Filestat stat = JnaUtils.getFilestat(GitBlitSuite.REPOSITORIES);
+			assertNotNull(stat);
+			assertTrue(stat.mode > 0);
+			assertTrue(stat.uid > 0);
+			assertTrue(stat.gid > 0);
+		}
+	}
+
+
+}

--
Gitblit v1.9.1