James Moger
2013-09-30 235ad956fa84cad4fac1b2e69a0c9e4f50376ea3
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;
@@ -79,7 +81,6 @@
import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -259,13 +260,187 @@
    * @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.
@@ -297,6 +472,7 @@
      list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder,
            onlyBare, searchSubfolders, depth, patterns));
      StringUtils.sortRepositorynames(list);
      list.remove(".git"); // issue-256
      return list;
   }
@@ -438,39 +614,56 @@
      }
      return false;
   }
   /**
    * Encapsulates the result of cloning or pulling from a repository.
    */
   public static class LastChange {
      public Date when;
      public String who;
      LastChange() {
         when = new Date(0);
      }
      LastChange(long lastModified) {
         this.when = new Date(lastModified);
      }
   }
   /**
    * Returns the date of the most recent commit on a branch. If the repository
    * does not exist Date(0) is returned. If it does exist but is empty, the
    * last modified date of the repository folder is returned.
    * Returns the date and author of the most recent commit on a branch. If the
    * repository does not exist Date(0) is returned. If it does exist but is
    * empty, the last modified date of the repository folder is returned.
    * 
    * @param repository
    * @return
    * @return a LastChange object
    */
   public static Date getLastChange(Repository repository) {
   public static LastChange getLastChange(Repository repository) {
      if (!hasCommits(repository)) {
         // null repository
         if (repository == null) {
            return new Date(0);
            return new LastChange();
         }
         // fresh repository
         return new Date(repository.getDirectory().lastModified());
         return new LastChange(repository.getDirectory().lastModified());
      }
      List<RefModel> branchModels = getLocalBranches(repository, true, -1);
      if (branchModels.size() > 0) {
         // find most recent branch update
         Date lastChange = new Date(0);
         LastChange lastChange = new LastChange();
         for (RefModel branchModel : branchModels) {
            if (branchModel.getDate().after(lastChange)) {
               lastChange = branchModel.getDate();
            if (branchModel.getDate().after(lastChange.when)) {
               lastChange.when = branchModel.getDate();
               lastChange.who = branchModel.getAuthorIdent().getName();
            }
         }
         return lastChange;
      }
      
      // default to the repository folder modification date
      return new Date(repository.getDirectory().lastModified());
      return new LastChange(repository.getDirectory().lastModified());
   }
   /**
@@ -548,6 +741,8 @@
      try {
         if (tree == null) {
            ObjectId object = getDefaultBranch(repository);
            if (object == null)
               return null;
            RevCommit commit = rw.parseCommit(object);
            tree = commit.getTree();
         }
@@ -705,7 +900,7 @@
      Collections.sort(list);
      return list;
   }
   /**
    * Returns the list of files changed in a specified commit. If the
    * repository does not exist or is empty, an empty list is returned.
@@ -716,6 +911,21 @@
    * @return list of files changed in a commit
    */
   public static List<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit) {
      return getFilesInCommit(repository, commit, true);
   }
   /**
    * Returns the list of files changed in a specified commit. If the
    * repository does not exist or is empty, an empty list is returned.
    *
    * @param repository
    * @param commit
    *            if null, HEAD is assumed.
    * @param calculateDiffStat
    *            if true, each PathChangeModel will have insertions/deletions
    * @return list of files changed in a commit
    */
   public static List<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) {
      List<PathChangeModel> list = new ArrayList<PathChangeModel>();
      if (!hasCommits(repository)) {
         return list;
@@ -740,26 +950,25 @@
            tw.release();
         } else {
            RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
            DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
            DiffStatFormatter df = new DiffStatFormatter(commit.getName());
            df.setRepository(repository);
            df.setDiffComparator(RawTextComparator.DEFAULT);
            df.setDetectRenames(true);
            List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
            for (DiffEntry diff : diffs) {
               String objectId = diff.getNewId().name();
               if (diff.getChangeType().equals(ChangeType.DELETE)) {
                  list.add(new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
                        .getNewMode().getBits(), objectId, commit.getId().getName(), diff
                        .getChangeType()));
               } else if (diff.getChangeType().equals(ChangeType.RENAME)) {
                  list.add(new PathChangeModel(diff.getOldPath(), diff.getNewPath(), 0, diff
                        .getNewMode().getBits(), objectId, commit.getId().getName(), diff
                        .getChangeType()));
               } else {
                  list.add(new PathChangeModel(diff.getNewPath(), diff.getNewPath(), 0, diff
                        .getNewMode().getBits(), objectId, commit.getId().getName(), diff
                        .getChangeType()));
               // create the path change model
               PathChangeModel pcm = PathChangeModel.from(diff, commit.getName());
               if (calculateDiffStat) {
                  // update file diffstats
                  df.format(diff);
                  PathChangeModel pathStat = df.getDiffStat().getPath(pcm.path);
                  if (pathStat != null) {
                     pcm.insertions = pathStat.insertions;
                     pcm.deletions = pathStat.deletions;
                  }
               }
               list.add(pcm);
            }
         }
      } catch (Throwable t) {
@@ -794,20 +1003,8 @@
         List<DiffEntry> diffEntries = df.scan(startCommit.getTree(), endCommit.getTree());
         for (DiffEntry diff : diffEntries) {
            if (diff.getChangeType().equals(ChangeType.DELETE)) {
               list.add(new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
                     .getNewMode().getBits(), diff.getOldId().name(), null, diff
                     .getChangeType()));
            } else if (diff.getChangeType().equals(ChangeType.RENAME)) {
               list.add(new PathChangeModel(diff.getOldPath(), diff.getNewPath(), 0, diff
                     .getNewMode().getBits(), diff.getNewId().name(), null, diff
                     .getChangeType()));
            } else {
               list.add(new PathChangeModel(diff.getNewPath(), diff.getNewPath(), 0, diff
                     .getNewMode().getBits(), diff.getNewId().name(), null, diff
                     .getChangeType()));
            }
            PathChangeModel pcm = PathChangeModel.from(diff,  null);
            list.add(pcm);
         }         
         Collections.sort(list);
      } catch (Throwable t) {
@@ -1146,14 +1343,17 @@
    */
   public static List<RevCommit> searchRevlogs(Repository repository, String objectId,
         String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) {
      final String lcValue = value.toLowerCase();
      List<RevCommit> list = new ArrayList<RevCommit>();
      if (StringUtils.isEmpty(value)) {
         return list;
      }
      if (maxCount == 0) {
         return list;
      }
      if (!hasCommits(repository)) {
         return list;
      }
      final String lcValue = value.toLowerCase();
      try {
         // resolve branch
         ObjectId branchObject;