/* * 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.utils; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; 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; import org.eclipse.jgit.api.TagCommand; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.StopWalkException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.BlobBasedConfig; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; 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.merge.MergeStrategy; import org.eclipse.jgit.merge.RecursiveMerger; import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter; import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.OrTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.PathSuffixFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.GitBlit; import com.gitblit.GitBlitException; import com.gitblit.manager.GitblitManager; import com.gitblit.models.FilestoreModel; import com.gitblit.models.GitNote; import com.gitblit.models.PathModel; import com.gitblit.models.PathModel.PathChangeModel; import com.gitblit.models.RefModel; import com.gitblit.models.SubmoduleModel; import com.gitblit.servlet.FilestoreServlet; import com.google.common.base.Strings; /** * Collection of static methods for retrieving information from a repository. * * @author James Moger * */ public class JGitUtils { static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class); /** * Log an error message and exception. * * @param t * @param repository * if repository is not null it MUST be the {0} parameter in the * pattern. * @param pattern * @param objects */ private static void error(Throwable t, Repository repository, String pattern, Object... objects) { List parameters = new ArrayList(); if (objects != null && objects.length > 0) { for (Object o : objects) { parameters.add(o); } } if (repository != null) { parameters.add(0, repository.getDirectory().getAbsolutePath()); } LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t); } /** * Returns the displayable name of the person in the form "Real Name ". If the email address is empty, just "Real Name" is returned. * * @param person * @return "Real Name " or "Real Name" */ public static String getDisplayName(PersonIdent person) { if (StringUtils.isEmpty(person.getEmailAddress())) { return person.getName(); } final StringBuilder r = new StringBuilder(); r.append(person.getName()); r.append(" <"); r.append(person.getEmailAddress()); r.append('>'); return r.toString().trim(); } /** * Encapsulates the result of cloning or pulling from a repository. */ public static class CloneResult { public String name; public FetchResult fetchResult; public boolean createdRepository; } /** * Clone or Fetch a repository. If the local repository does not exist, * clone is called. If the repository does exist, fetch is called. By * default the clone/fetch retrieves the remote heads, tags, and notes. * * @param repositoriesFolder * @param name * @param fromUrl * @return CloneResult * @throws Exception */ public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl) throws Exception { return cloneRepository(repositoriesFolder, name, fromUrl, true, null); } /** * Clone or Fetch a repository. If the local repository does not exist, * clone is called. If the repository does exist, fetch is called. By * default the clone/fetch retrieves the remote heads, tags, and notes. * * @param repositoriesFolder * @param name * @param fromUrl * @param bare * @param credentialsProvider * @return CloneResult * @throws Exception */ public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl, boolean bare, CredentialsProvider credentialsProvider) throws Exception { CloneResult result = new CloneResult(); if (bare) { // bare repository, ensure .git suffix if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { name += Constants.DOT_GIT_EXT; } } else { // normal repository, strip .git suffix if (name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT)); } } result.name = name; File folder = new File(repositoriesFolder, name); if (folder.exists()) { File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED); Repository repository = new FileRepositoryBuilder().setGitDir(gitDir).build(); result.fetchResult = fetchRepository(credentialsProvider, repository); repository.close(); } else { CloneCommand clone = new CloneCommand(); clone.setBare(bare); clone.setCloneAllBranches(true); clone.setURI(fromUrl); clone.setDirectory(folder); if (credentialsProvider != null) { clone.setCredentialsProvider(credentialsProvider); } Repository repository = clone.call().getRepository(); // Now we have to fetch because CloneCommand doesn't fetch // refs/notes nor does it allow manual RefSpec. result.createdRepository = true; result.fetchResult = fetchRepository(credentialsProvider, repository); repository.close(); } return result; } /** * Fetch updates from the remote repository. If refSpecs is unspecifed, * remote heads, tags, and notes are retrieved. * * @param credentialsProvider * @param repository * @param refSpecs * @return FetchResult * @throws Exception */ public static FetchResult fetchRepository(CredentialsProvider credentialsProvider, Repository repository, RefSpec... refSpecs) throws Exception { Git git = new Git(repository); FetchCommand fetch = git.fetch(); List specs = new ArrayList(); if (refSpecs == null || refSpecs.length == 0) { specs.add(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); specs.add(new RefSpec("+refs/tags/*:refs/tags/*")); specs.add(new RefSpec("+refs/notes/*:refs/notes/*")); } else { specs.addAll(Arrays.asList(refSpecs)); } if (credentialsProvider != null) { fetch.setCredentialsProvider(credentialsProvider); } fetch.setRefSpecs(specs); FetchResult fetchRes = fetch.call(); return fetchRes; } /** * Creates a bare repository. * * @param repositoriesFolder * @param name * @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 { 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 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. * * @param repositoriesFolder * @param onlyBare * if true, only bare repositories repositories are listed. If * false all repositories are included. * @param searchSubfolders * recurse into subfolders to find grouped repositories * @param depth * optional recursion depth, -1 = infinite recursion * @param exclusions * list of regex exclusions for matching to folder names * @return list of repository names */ public static List getRepositoryList(File repositoriesFolder, boolean onlyBare, boolean searchSubfolders, int depth, List exclusions) { List list = new ArrayList(); if (repositoriesFolder == null || !repositoriesFolder.exists()) { return list; } List patterns = new ArrayList(); if (!ArrayUtils.isEmpty(exclusions)) { for (String regex : exclusions) { patterns.add(Pattern.compile(regex)); } } list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder, onlyBare, searchSubfolders, depth, patterns)); StringUtils.sortRepositorynames(list); list.remove(".git"); // issue-256 return list; } /** * Recursive function to find git repositories. * * @param basePath * basePath is stripped from the repository name as repositories * are relative to this path * @param searchFolder * @param onlyBare * if true only bare repositories will be listed. if false all * repositories are included. * @param searchSubfolders * recurse into subfolders to find grouped repositories * @param depth * recursion depth, -1 = infinite recursion * @param patterns * list of regex patterns for matching to folder names * @return */ private static List getRepositoryList(String basePath, File searchFolder, boolean onlyBare, boolean searchSubfolders, int depth, List patterns) { File baseFile = new File(basePath); List list = new ArrayList(); if (depth == 0) { return list; } int nextDepth = (depth == -1) ? -1 : depth - 1; for (File file : searchFolder.listFiles()) { if (file.isDirectory()) { boolean exclude = false; for (Pattern pattern : patterns) { String path = FileUtils.getRelativePath(baseFile, file).replace('\\', '/'); if (pattern.matcher(path).matches()) { LOGGER.debug(MessageFormat.format("excluding {0} because of rule {1}", path, pattern.pattern())); exclude = true; break; } } if (exclude) { // skip to next file continue; } File gitDir = FileKey.resolve(new File(searchFolder, file.getName()), FS.DETECTED); if (gitDir != null) { if (onlyBare && gitDir.getName().equals(".git")) { continue; } if (gitDir.equals(file) || gitDir.getParentFile().equals(file)) { // determine repository name relative to base path String repository = FileUtils.getRelativePath(baseFile, file); list.add(repository); } else if (searchSubfolders && file.canRead()) { // look for repositories in subfolders list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders, nextDepth, patterns)); } } else if (searchSubfolders && file.canRead()) { // look for repositories in subfolders list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders, nextDepth, patterns)); } } } return list; } /** * Returns the first commit on a branch. If the repository does not exist or * is empty, null is returned. * * @param repository * @param branch * if unspecified, HEAD is assumed. * @return RevCommit */ public static RevCommit getFirstCommit(Repository repository, String branch) { if (!hasCommits(repository)) { return null; } RevCommit commit = null; try { // resolve branch ObjectId branchObject; if (StringUtils.isEmpty(branch)) { branchObject = getDefaultBranch(repository); } else { branchObject = repository.resolve(branch); } RevWalk walk = new RevWalk(repository); walk.sort(RevSort.REVERSE); RevCommit head = walk.parseCommit(branchObject); walk.markStart(head); commit = walk.next(); walk.dispose(); } catch (Throwable t) { error(t, repository, "{0} failed to determine first commit"); } return commit; } /** * Returns the date of the first commit on a branch. If the repository does * not exist, Date(0) is returned. If the repository does exist bit is * empty, the last modified date of the repository folder is returned. * * @param repository * @param branch * if unspecified, HEAD is assumed. * @return Date of the first commit on a branch */ public static Date getFirstChange(Repository repository, String branch) { RevCommit commit = getFirstCommit(repository, branch); if (commit == null) { if (repository == null || !repository.getDirectory().exists()) { return new Date(0); } // fresh repository return new Date(repository.getDirectory().lastModified()); } return getCommitDate(commit); } /** * Determine if a repository has any commits. This is determined by checking * the for loose and packed objects. * * @param repository * @return true if the repository has commits */ public static boolean hasCommits(Repository repository) { if (repository != null && repository.getDirectory().exists()) { return (new File(repository.getDirectory(), "objects").list().length > 2) || (new File(repository.getDirectory(), "objects/pack").list().length > 0); } 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 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 a LastChange object */ public static LastChange getLastChange(Repository repository) { if (!hasCommits(repository)) { // null repository if (repository == null) { return new LastChange(); } // fresh repository return new LastChange(repository.getDirectory().lastModified()); } List branchModels = getLocalBranches(repository, true, -1); if (branchModels.size() > 0) { // find most recent branch update LastChange lastChange = new LastChange(); for (RefModel branchModel : branchModels) { 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 LastChange(repository.getDirectory().lastModified()); } /** * Retrieves a Java Date from a Git commit. * * @param commit * @return date of the commit or Date(0) if the commit is null */ public static Date getCommitDate(RevCommit commit) { if (commit == null) { return new Date(0); } return new Date(commit.getCommitTime() * 1000L); } /** * Retrieves a Java Date from a Git commit. * * @param commit * @return date of the commit or Date(0) if the commit is null */ public static Date getAuthorDate(RevCommit commit) { if (commit == null) { return new Date(0); } if (commit.getAuthorIdent() != null) { return commit.getAuthorIdent().getWhen(); } return getCommitDate(commit); } /** * Returns the specified commit from the repository. If the repository does * not exist or is empty, null is returned. * * @param repository * @param objectId * if unspecified, HEAD is assumed. * @return RevCommit */ public static RevCommit getCommit(Repository repository, String objectId) { if (!hasCommits(repository)) { return null; } RevCommit commit = null; RevWalk walk = null; try { // resolve object id ObjectId branchObject; if (StringUtils.isEmpty(objectId) || "HEAD".equalsIgnoreCase(objectId)) { branchObject = getDefaultBranch(repository); } else { branchObject = repository.resolve(objectId); } if (branchObject == null) { return null; } walk = new RevWalk(repository); RevCommit rev = walk.parseCommit(branchObject); commit = rev; } catch (Throwable t) { error(t, repository, "{0} failed to get commit {1}", objectId); } finally { if (walk != null) { walk.dispose(); } } return commit; } /** * Retrieves the raw byte content of a file in the specified tree. * * @param repository * @param tree * if null, the RevTree from HEAD is assumed. * @param path * @return content as a byte [] */ public static byte[] getByteContent(Repository repository, RevTree tree, final String path, boolean throwError) { RevWalk rw = new RevWalk(repository); TreeWalk tw = new TreeWalk(repository); tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path))); byte[] content = null; try { if (tree == null) { ObjectId object = getDefaultBranch(repository); if (object == null) return null; RevCommit commit = rw.parseCommit(object); tree = commit.getTree(); } tw.reset(tree); while (tw.next()) { if (tw.isSubtree() && !path.equals(tw.getPathString())) { tw.enterSubtree(); continue; } ObjectId entid = tw.getObjectId(0); FileMode entmode = tw.getFileMode(0); if (entmode != FileMode.GITLINK) { ObjectLoader ldr = repository.open(entid, Constants.OBJ_BLOB); content = ldr.getCachedBytes(); } } } catch (Throwable t) { if (throwError) { error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name()); } } finally { rw.dispose(); tw.close(); } return content; } /** * Returns the UTF-8 string content of a file in the specified tree. * * @param repository * @param tree * if null, the RevTree from HEAD is assumed. * @param blobPath * @param charsets optional * @return UTF-8 string content */ public static String getStringContent(Repository repository, RevTree tree, String blobPath, String... charsets) { byte[] content = getByteContent(repository, tree, blobPath, true); if (content == null) { return null; } return StringUtils.decodeString(content, charsets); } /** * Gets the raw byte content of the specified blob object. * * @param repository * @param objectId * @return byte [] blob content */ public static byte[] getByteContent(Repository repository, String objectId) { RevWalk rw = new RevWalk(repository); byte[] content = null; try { RevBlob blob = rw.lookupBlob(ObjectId.fromString(objectId)); ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB); content = ldr.getCachedBytes(); } catch (Throwable t) { error(t, repository, "{0} can't find blob {1}", objectId); } finally { rw.dispose(); } return content; } /** * Gets the UTF-8 string content of the blob specified by objectId. * * @param repository * @param objectId * @param charsets optional * @return UTF-8 string content */ public static String getStringContent(Repository repository, String objectId, String... charsets) { byte[] content = getByteContent(repository, objectId); if (content == null) { return null; } return StringUtils.decodeString(content, charsets); } /** * Returns the list of files in the specified folder at the specified * commit. If the repository does not exist or is empty, an empty list is * returned. * * @param repository * @param path * if unspecified, root folder is assumed. * @param commit * if null, HEAD is assumed. * @return list of files in specified path */ public static List getFilesInPath(Repository repository, String path, RevCommit commit) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } if (commit == null) { commit = getCommit(repository, null); } final TreeWalk tw = new TreeWalk(repository); try { tw.addTree(commit.getTree()); if (!StringUtils.isEmpty(path)) { PathFilter f = PathFilter.create(path); tw.setFilter(f); tw.setRecursive(false); boolean foundFolder = false; while (tw.next()) { if (!foundFolder && tw.isSubtree()) { tw.enterSubtree(); } if (tw.getPathString().equals(path)) { foundFolder = true; continue; } if (foundFolder) { list.add(getPathModel(tw, path, commit)); } } } else { tw.setRecursive(false); while (tw.next()) { list.add(getPathModel(tw, null, commit)); } } } catch (IOException e) { error(e, repository, "{0} failed to get files for commit {1}", commit.getName()); } finally { tw.close(); } Collections.sort(list); return list; } /** * Returns the list of files in the specified folder at the specified * commit. If the repository does not exist or is empty, an empty list is * returned. * * This is modified version that implements path compression feature. * * @param repository * @param path * if unspecified, root folder is assumed. * @param commit * if null, HEAD is assumed. * @return list of files in specified path */ public static List getFilesInPath2(Repository repository, String path, RevCommit commit) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } if (commit == null) { commit = getCommit(repository, null); } final TreeWalk tw = new TreeWalk(repository); try { tw.addTree(commit.getTree()); final boolean isPathEmpty = Strings.isNullOrEmpty(path); if (!isPathEmpty) { PathFilter f = PathFilter.create(path); tw.setFilter(f); } tw.setRecursive(true); List paths = new ArrayList<>(); while (tw.next()) { String child = isPathEmpty ? tw.getPathString() : tw.getPathString().replaceFirst(String.format("%s/", path), ""); paths.add(child); } for(String p: PathUtils.compressPaths(paths)) { String pathString = isPathEmpty ? p : String.format("%s/%s", path, p); list.add(getPathModel(repository, pathString, path, commit)); } } catch (IOException e) { error(e, repository, "{0} failed to get files for commit {1}", commit.getName()); } finally { tw.close(); } 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. * * @param repository * @param commit * if null, HEAD is assumed. * @return list of files changed in a commit */ public static List 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 getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } RevWalk rw = new RevWalk(repository); try { if (commit == null) { ObjectId object = getDefaultBranch(repository); commit = rw.parseCommit(object); } if (commit.getParentCount() == 0) { TreeWalk tw = new TreeWalk(repository); tw.reset(); tw.setRecursive(true); tw.addTree(commit.getTree()); while (tw.next()) { long size = 0; FilestoreModel filestoreItem = null; ObjectId objectId = tw.getObjectId(0); try { if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB); if (isPossibleFilestoreItem(size)) { filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId)); } } } catch (Throwable t) { error(t, null, "failed to retrieve blob size for " + tw.getPathString()); } list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(),filestoreItem, size, tw .getRawMode(0), objectId.getName(), commit.getId().getName(), ChangeType.ADD)); } tw.close(); } else { RevCommit parent = rw.parseCommit(commit.getParent(0).getId()); DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository); df.setRepository(repository); df.setDiffComparator(RawTextComparator.DEFAULT); df.setDetectRenames(true); List diffs = df.scan(parent.getTree(), commit.getTree()); for (DiffEntry diff : diffs) { // create the path change model PathChangeModel pcm = PathChangeModel.from(diff, commit.getName(), repository); 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) { error(t, repository, "{0} failed to determine files in commit!"); } finally { rw.dispose(); } 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. * * @param repository * @param startCommit * earliest commit * @param endCommit * most recent commit. if null, HEAD is assumed. * @return list of files changed in a commit range */ public static List getFilesInRange(Repository repository, String startCommit, String endCommit) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } try { ObjectId startRange = repository.resolve(startCommit); ObjectId endRange = repository.resolve(endCommit); RevWalk rw = new RevWalk(repository); RevCommit start = rw.parseCommit(startRange); RevCommit end = rw.parseCommit(endRange); list.addAll(getFilesInRange(repository, start, end)); rw.close(); } catch (Throwable t) { error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit); } 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. * * @param repository * @param startCommit * earliest commit * @param endCommit * most recent commit. if null, HEAD is assumed. * @return list of files changed in a commit range */ public static List getFilesInRange(Repository repository, RevCommit startCommit, RevCommit endCommit) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } try { DiffFormatter df = new DiffFormatter(null); df.setRepository(repository); df.setDiffComparator(RawTextComparator.DEFAULT); df.setDetectRenames(true); List diffEntries = df.scan(startCommit.getTree(), endCommit.getTree()); for (DiffEntry diff : diffEntries) { PathChangeModel pcm = PathChangeModel.from(diff, endCommit.getName(), repository); list.add(pcm); } Collections.sort(list); } catch (Throwable t) { error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit); } return list; } /** * Returns the list of files in the repository on the default branch that * match one of the specified extensions. This is a CASE-SENSITIVE search. * If the repository does not exist or is empty, an empty list is returned. * * @param repository * @param extensions * @return list of files in repository with a matching extension */ public static List getDocuments(Repository repository, List extensions) { return getDocuments(repository, extensions, null); } /** * Returns the list of files in the repository in the specified commit that * match one of the specified extensions. This is a CASE-SENSITIVE search. * If the repository does not exist or is empty, an empty list is returned. * * @param repository * @param extensions * @param objectId * @return list of files in repository with a matching extension */ public static List getDocuments(Repository repository, List extensions, String objectId) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } RevCommit commit = getCommit(repository, objectId); final TreeWalk tw = new TreeWalk(repository); try { tw.addTree(commit.getTree()); if (extensions != null && extensions.size() > 0) { List suffixFilters = new ArrayList(); for (String extension : extensions) { if (extension.charAt(0) == '.') { suffixFilters.add(PathSuffixFilter.create(extension)); } else { // escape the . since this is a regexp filter suffixFilters.add(PathSuffixFilter.create("." + extension)); } } TreeFilter filter; if (suffixFilters.size() == 1) { filter = suffixFilters.get(0); } else { filter = OrTreeFilter.create(suffixFilters); } tw.setFilter(filter); tw.setRecursive(true); } while (tw.next()) { list.add(getPathModel(tw, null, commit)); } } catch (IOException e) { error(e, repository, "{0} failed to get documents for commit {1}", commit.getName()); } finally { tw.close(); } Collections.sort(list); return list; } /** * Returns a path model of the current file in the treewalk. * * @param tw * @param basePath * @param commit * @return a path model of the current file in the treewalk */ private static PathModel getPathModel(TreeWalk tw, String basePath, RevCommit commit) { String name; long size = 0; if (StringUtils.isEmpty(basePath)) { name = tw.getPathString(); } else { name = tw.getPathString().substring(basePath.length() + 1); } ObjectId objectId = tw.getObjectId(0); FilestoreModel filestoreItem = null; try { if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB); if (isPossibleFilestoreItem(size)) { filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId)); } } } catch (Throwable t) { error(t, null, "failed to retrieve blob size for " + tw.getPathString()); } return new PathModel(name, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(), objectId.getName(), commit.getName()); } public static boolean isPossibleFilestoreItem(long size) { return ( (size >= com.gitblit.Constants.LEN_FILESTORE_META_MIN) && (size <= com.gitblit.Constants.LEN_FILESTORE_META_MAX)); } /** * * @return Representative FilestoreModel if valid, otherwise null */ public static FilestoreModel getFilestoreItem(ObjectLoader obj){ try { final byte[] blob = obj.getCachedBytes(com.gitblit.Constants.LEN_FILESTORE_META_MAX); final String meta = new String(blob, "UTF-8"); return FilestoreModel.fromMetaString(meta); } catch (LargeObjectException e) { //Intentionally failing silent } catch (Exception e) { error(e, null, "failed to retrieve filestoreItem " + obj.toString()); } return null; } /** * Returns a path model by path string * * @param repo * @param path * @param filter * @param commit * @return a path model of the specified object */ private static PathModel getPathModel(Repository repo, String path, String filter, RevCommit commit) throws IOException { long size = 0; FilestoreModel filestoreItem = null; TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree()); String pathString = path; if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { pathString = PathUtils.getLastPathComponent(pathString); size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB); if (isPossibleFilestoreItem(size)) { filestoreItem = getFilestoreItem(tw.getObjectReader().open(tw.getObjectId(0))); } } else if (tw.isSubtree()) { // do not display dirs that are behind in the path if (!Strings.isNullOrEmpty(filter)) { pathString = path.replaceFirst(filter + "/", ""); } // remove the last slash from path in displayed link if (pathString != null && pathString.charAt(pathString.length()-1) == '/') { pathString = pathString.substring(0, pathString.length()-1); } } return new PathModel(pathString, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(), tw.getObjectId(0).getName(), commit.getName()); } /** * Returns a permissions representation of the mode bits. * * @param mode * @return string representation of the mode bits */ public static String getPermissionsFromMode(int mode) { if (FileMode.TREE.equals(mode)) { return "drwxr-xr-x"; } else if (FileMode.REGULAR_FILE.equals(mode)) { return "-rw-r--r--"; } else if (FileMode.EXECUTABLE_FILE.equals(mode)) { return "-rwxr-xr-x"; } else if (FileMode.SYMLINK.equals(mode)) { return "symlink"; } else if (FileMode.GITLINK.equals(mode)) { return "submodule"; } return "missing"; } /** * Returns a list of commits since the minimum date starting from the * specified object id. * * @param repository * @param objectId * if unspecified, HEAD is assumed. * @param minimumDate * @return list of commits */ public static List getRevLog(Repository repository, String objectId, Date minimumDate) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } try { // resolve branch ObjectId branchObject; if (StringUtils.isEmpty(objectId)) { branchObject = getDefaultBranch(repository); } else { branchObject = repository.resolve(objectId); } RevWalk rw = new RevWalk(repository); rw.markStart(rw.parseCommit(branchObject)); rw.setRevFilter(CommitTimeRevFilter.after(minimumDate)); Iterable revlog = rw; for (RevCommit rev : revlog) { list.add(rev); } rw.dispose(); } catch (Throwable t) { error(t, repository, "{0} failed to get {1} revlog for minimum date {2}", objectId, minimumDate); } return list; } /** * Returns a list of commits starting from HEAD and working backwards. * * @param repository * @param maxCount * if < 0, all commits for the repository are returned. * @return list of commits */ public static List getRevLog(Repository repository, int maxCount) { return getRevLog(repository, null, 0, maxCount); } /** * Returns a list of commits starting from the specified objectId using an * offset and maxCount for paging. This is similar to LIMIT n OFFSET p in * SQL. If the repository does not exist or is empty, an empty list is * returned. * * @param repository * @param objectId * if unspecified, HEAD is assumed. * @param offset * @param maxCount * if < 0, all commits are returned. * @return a paged list of commits */ public static List getRevLog(Repository repository, String objectId, int offset, int maxCount) { return getRevLog(repository, objectId, null, offset, maxCount); } /** * Returns a list of commits for the repository or a path within the * repository. Caller may specify ending revision with objectId. Caller may * specify offset and maxCount to achieve pagination of results. If the * repository does not exist or is empty, an empty list is returned. * * @param repository * @param objectId * if unspecified, HEAD is assumed. * @param path * if unspecified, commits for repository are returned. If * specified, commits for the path are returned. * @param offset * @param maxCount * if < 0, all commits are returned. * @return a paged list of commits */ public static List getRevLog(Repository repository, String objectId, String path, int offset, int maxCount) { List list = new ArrayList(); if (maxCount == 0) { return list; } if (!hasCommits(repository)) { return list; } try { // resolve branch ObjectId startRange = null; ObjectId endRange; if (StringUtils.isEmpty(objectId)) { endRange = getDefaultBranch(repository); } else { if( objectId.contains("..") ) { // range expression String[] parts = objectId.split("\\.\\."); startRange = repository.resolve(parts[0]); endRange = repository.resolve(parts[1]); } else { // objectid endRange= repository.resolve(objectId); } } if (endRange == null) { return list; } RevWalk rw = new RevWalk(repository); rw.markStart(rw.parseCommit(endRange)); if (startRange != null) { rw.markUninteresting(rw.parseCommit(startRange)); } if (!StringUtils.isEmpty(path)) { TreeFilter filter = AndTreeFilter.create( PathFilterGroup.createFromStrings(Collections.singleton(path)), TreeFilter.ANY_DIFF); rw.setTreeFilter(filter); } Iterable revlog = rw; if (offset > 0) { int count = 0; for (RevCommit rev : revlog) { count++; if (count > offset) { list.add(rev); if (maxCount > 0 && list.size() == maxCount) { break; } } } } else { for (RevCommit rev : revlog) { list.add(rev); if (maxCount > 0 && list.size() == maxCount) { break; } } } rw.dispose(); } catch (Throwable t) { error(t, repository, "{0} failed to get {1} revlog for path {2}", objectId, path); } return list; } /** * Returns a list of commits for the repository within the range specified * by startRangeId and endRangeId. If the repository does not exist or is * empty, an empty list is returned. * * @param repository * @param startRangeId * the first commit (not included in results) * @param endRangeId * the end commit (included in results) * @return a list of commits */ public static List getRevLog(Repository repository, String startRangeId, String endRangeId) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } try { ObjectId endRange = repository.resolve(endRangeId); ObjectId startRange = repository.resolve(startRangeId); RevWalk rw = new RevWalk(repository); rw.markStart(rw.parseCommit(endRange)); if (startRange.equals(ObjectId.zeroId())) { // maybe this is a tag or an orphan branch list.add(rw.parseCommit(endRange)); rw.dispose(); return list; } else { rw.markUninteresting(rw.parseCommit(startRange)); } Iterable revlog = rw; for (RevCommit rev : revlog) { list.add(rev); } rw.dispose(); } catch (Throwable t) { error(t, repository, "{0} failed to get revlog for {1}..{2}", startRangeId, endRangeId); } return list; } /** * Search the commit history for a case-insensitive match to the value. * Search results require a specified SearchType of AUTHOR, COMMITTER, or * COMMIT. Results may be paginated using offset and maxCount. If the * repository does not exist or is empty, an empty list is returned. * * @param repository * @param objectId * if unspecified, HEAD is assumed. * @param value * @param type * AUTHOR, COMMITTER, COMMIT * @param offset * @param maxCount * if < 0, all matches are returned * @return matching list of commits */ public static List searchRevlogs(Repository repository, String objectId, String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) { List list = new ArrayList(); 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; if (StringUtils.isEmpty(objectId)) { branchObject = getDefaultBranch(repository); } else { branchObject = repository.resolve(objectId); } RevWalk rw = new RevWalk(repository); rw.setRevFilter(new RevFilter() { @Override public RevFilter clone() { // FindBugs complains about this method name. // This is part of JGit design and unrelated to Cloneable. return this; } @Override public boolean include(RevWalk walker, RevCommit commit) throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, IOException { boolean include = false; switch (type) { case AUTHOR: include = (commit.getAuthorIdent().getName().toLowerCase().indexOf(lcValue) > -1) || (commit.getAuthorIdent().getEmailAddress().toLowerCase() .indexOf(lcValue) > -1); break; case COMMITTER: include = (commit.getCommitterIdent().getName().toLowerCase() .indexOf(lcValue) > -1) || (commit.getCommitterIdent().getEmailAddress().toLowerCase() .indexOf(lcValue) > -1); break; case COMMIT: include = commit.getFullMessage().toLowerCase().indexOf(lcValue) > -1; break; } return include; } }); rw.markStart(rw.parseCommit(branchObject)); Iterable revlog = rw; if (offset > 0) { int count = 0; for (RevCommit rev : revlog) { count++; if (count > offset) { list.add(rev); if (maxCount > 0 && list.size() == maxCount) { break; } } } } else { for (RevCommit rev : revlog) { list.add(rev); if (maxCount > 0 && list.size() == maxCount) { break; } } } rw.dispose(); } catch (Throwable t) { error(t, repository, "{0} failed to {1} search revlogs for {2}", type.name(), value); } return list; } /** * Returns the default branch to use for a repository. Normally returns * whatever branch HEAD points to, but if HEAD points to nothing it returns * the most recently updated branch. * * @param repository * @return the objectid of a branch * @throws Exception */ public static ObjectId getDefaultBranch(Repository repository) throws Exception { ObjectId object = repository.resolve(Constants.HEAD); if (object == null) { // no HEAD // perhaps non-standard repository, try local branches List branchModels = getLocalBranches(repository, true, -1); if (branchModels.size() > 0) { // use most recently updated branch RefModel branch = null; Date lastDate = new Date(0); for (RefModel branchModel : branchModels) { if (branchModel.getDate().after(lastDate)) { branch = branchModel; lastDate = branch.getDate(); } } object = branch.getReferencedObjectId(); } } return object; } /** * Returns the target of the symbolic HEAD reference for a repository. * Normally returns a branch reference name, but when HEAD is detached, * the commit is matched against the known tags. The most recent matching * tag ref name will be returned if it references the HEAD commit. If * no match is found, the SHA1 is returned. * * @param repository * @return the ref name or the SHA1 for a detached HEAD */ public static String getHEADRef(Repository repository) { String target = null; try { target = repository.getFullBranch(); } catch (Throwable t) { error(t, repository, "{0} failed to get symbolic HEAD target"); } return target; } /** * Sets the symbolic ref HEAD to the specified target ref. The * HEAD will be detached if the target ref is not a branch. * * @param repository * @param targetRef * @return true if successful */ public static boolean setHEADtoRef(Repository repository, String targetRef) { try { // detach HEAD if target ref is not a branch boolean detach = !targetRef.startsWith(Constants.R_HEADS); RefUpdate.Result result; RefUpdate head = repository.updateRef(Constants.HEAD, detach); if (detach) { // Tag RevCommit commit = getCommit(repository, targetRef); head.setNewObjectId(commit.getId()); result = head.forceUpdate(); } else { result = head.link(targetRef); } switch (result) { case NEW: case FORCED: case NO_CHANGE: case FAST_FORWARD: return true; default: LOGGER.error(MessageFormat.format("{0} HEAD update to {1} returned result {2}", repository.getDirectory().getAbsolutePath(), targetRef, result)); } } catch (Throwable t) { error(t, repository, "{0} failed to set HEAD to {1}", targetRef); } return false; } /** * Sets the local branch ref to point to the specified commit id. * * @param repository * @param branch * @param commitId * @return true if successful */ public static boolean setBranchRef(Repository repository, String branch, String commitId) { String branchName = branch; if (!branchName.startsWith(Constants.R_REFS)) { branchName = Constants.R_HEADS + branch; } try { RefUpdate refUpdate = repository.updateRef(branchName, false); refUpdate.setNewObjectId(ObjectId.fromString(commitId)); RefUpdate.Result result = refUpdate.forceUpdate(); switch (result) { case NEW: case FORCED: case NO_CHANGE: case FAST_FORWARD: return true; default: LOGGER.error(MessageFormat.format("{0} {1} update to {2} returned result {3}", repository.getDirectory().getAbsolutePath(), branchName, commitId, result)); } } catch (Throwable t) { error(t, repository, "{0} failed to set {1} to {2}", branchName, commitId); } return false; } /** * Deletes the specified branch ref. * * @param repository * @param branch * @return true if successful */ public static boolean deleteBranchRef(Repository repository, String branch) { try { RefUpdate refUpdate = repository.updateRef(branch, false); refUpdate.setForceUpdate(true); RefUpdate.Result result = refUpdate.delete(); switch (result) { case NEW: case FORCED: case NO_CHANGE: case FAST_FORWARD: return true; default: LOGGER.error(MessageFormat.format("{0} failed to delete to {1} returned result {2}", repository.getDirectory().getAbsolutePath(), branch, result)); } } catch (Throwable t) { error(t, repository, "{0} failed to delete {1}", branch); } return false; } /** * Get the full branch and tag ref names for any potential HEAD targets. * * @param repository * @return a list of ref names */ public static List getAvailableHeadTargets(Repository repository) { List targets = new ArrayList(); for (RefModel branchModel : JGitUtils.getLocalBranches(repository, true, -1)) { targets.add(branchModel.getName()); } for (RefModel tagModel : JGitUtils.getTags(repository, true, -1)) { targets.add(tagModel.getName()); } return targets; } /** * Returns all refs grouped by their associated object id. * * @param repository * @return all refs grouped by their referenced object id */ public static Map> getAllRefs(Repository repository) { return getAllRefs(repository, true); } /** * Returns all refs grouped by their associated object id. * * @param repository * @param includeRemoteRefs * @return all refs grouped by their referenced object id */ public static Map> getAllRefs(Repository repository, boolean includeRemoteRefs) { List list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1); Map> refs = new HashMap>(); for (RefModel ref : list) { if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) { continue; } ObjectId objectid = ref.getReferencedObjectId(); if (!refs.containsKey(objectid)) { refs.put(objectid, new ArrayList()); } refs.get(objectid).add(ref); } return refs; } /** * Returns the list of tags in the repository. If repository does not exist * or is empty, an empty list is returned. * * @param repository * @param fullName * if true, /refs/tags/yadayadayada is returned. If false, * yadayadayada is returned. * @param maxCount * if < 0, all tags are returned * @return list of tags */ public static List getTags(Repository repository, boolean fullName, int maxCount) { return getRefs(repository, Constants.R_TAGS, fullName, maxCount); } /** * Returns the list of tags in the repository. If repository does not exist * or is empty, an empty list is returned. * * @param repository * @param fullName * if true, /refs/tags/yadayadayada is returned. If false, * yadayadayada is returned. * @param maxCount * if < 0, all tags are returned * @param offset * if maxCount provided sets the starting point of the records to return * @return list of tags */ public static List getTags(Repository repository, boolean fullName, int maxCount, int offset) { return getRefs(repository, Constants.R_TAGS, fullName, maxCount, offset); } /** * Returns the list of local branches in the repository. If repository does * not exist or is empty, an empty list is returned. * * @param repository * @param fullName * if true, /refs/heads/yadayadayada is returned. If false, * yadayadayada is returned. * @param maxCount * if < 0, all local branches are returned * @return list of local branches */ public static List getLocalBranches(Repository repository, boolean fullName, int maxCount) { return getRefs(repository, Constants.R_HEADS, fullName, maxCount); } /** * Returns the list of remote branches in the repository. If repository does * not exist or is empty, an empty list is returned. * * @param repository * @param fullName * if true, /refs/remotes/yadayadayada is returned. If false, * yadayadayada is returned. * @param maxCount * if < 0, all remote branches are returned * @return list of remote branches */ public static List getRemoteBranches(Repository repository, boolean fullName, int maxCount) { return getRefs(repository, Constants.R_REMOTES, fullName, maxCount); } /** * Returns the list of note branches. If repository does not exist or is * empty, an empty list is returned. * * @param repository * @param fullName * if true, /refs/notes/yadayadayada is returned. If false, * yadayadayada is returned. * @param maxCount * if < 0, all note branches are returned * @return list of note branches */ public static List getNoteBranches(Repository repository, boolean fullName, int maxCount) { return getRefs(repository, Constants.R_NOTES, fullName, maxCount); } /** * Returns the list of refs in the specified base ref. If repository does * not exist or is empty, an empty list is returned. * * @param repository * @param fullName * if true, /refs/yadayadayada is returned. If false, * yadayadayada is returned. * @return list of refs */ public static List getRefs(Repository repository, String baseRef) { return getRefs(repository, baseRef, true, -1); } /** * Returns a list of references in the repository matching "refs". If the * repository is null or empty, an empty list is returned. * * @param repository * @param refs * if unspecified, all refs are returned * @param fullName * if true, /refs/something/yadayadayada is returned. If false, * yadayadayada is returned. * @param maxCount * if < 0, all references are returned * @return list of references */ private static List getRefs(Repository repository, String refs, boolean fullName, int maxCount) { return getRefs(repository, refs, fullName, maxCount, 0); } /** * Returns a list of references in the repository matching "refs". If the * repository is null or empty, an empty list is returned. * * @param repository * @param refs * if unspecified, all refs are returned * @param fullName * if true, /refs/something/yadayadayada is returned. If false, * yadayadayada is returned. * @param maxCount * if < 0, all references are returned * @param offset * if maxCount provided sets the starting point of the records to return * @return list of references */ private static List getRefs(Repository repository, String refs, boolean fullName, int maxCount, int offset) { List list = new ArrayList(); if (maxCount == 0) { return list; } if (!hasCommits(repository)) { return list; } try { Map map = repository.getRefDatabase().getRefs(refs); RevWalk rw = new RevWalk(repository); for (Entry entry : map.entrySet()) { Ref ref = entry.getValue(); RevObject object = rw.parseAny(ref.getObjectId()); String name = entry.getKey(); if (fullName && !StringUtils.isEmpty(refs)) { name = refs + name; } list.add(new RefModel(name, ref, object)); } rw.dispose(); Collections.sort(list); Collections.reverse(list); if (maxCount > 0 && list.size() > maxCount) { if (offset < 0) { offset = 0; } int endIndex = offset + maxCount; if (endIndex > list.size()) { endIndex = list.size(); } list = new ArrayList(list.subList(offset, endIndex)); } } catch (IOException e) { error(e, repository, "{0} failed to retrieve {1}", refs); } return list; } /** * Returns a RefModel for the gh-pages branch in the repository. If the * branch can not be found, null is returned. * * @param repository * @return a refmodel for the gh-pages branch or null */ public static RefModel getPagesBranch(Repository repository) { return getBranch(repository, "gh-pages"); } /** * Returns a RefModel for a specific branch name in the repository. If the * branch can not be found, null is returned. * * @param repository * @return a refmodel for the branch or null */ public static RefModel getBranch(Repository repository, String name) { RefModel branch = null; try { // search for the branch in local heads for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) { if (ref.reference.getName().endsWith(name)) { branch = ref; break; } } // search for the branch in remote heads if (branch == null) { for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) { if (ref.reference.getName().endsWith(name)) { branch = ref; break; } } } } catch (Throwable t) { LOGGER.error(MessageFormat.format("Failed to find {0} branch!", name), t); } return branch; } /** * Returns the list of submodules for this repository. * * @param repository * @param commit * @return list of submodules */ public static List getSubmodules(Repository repository, String commitId) { RevCommit commit = getCommit(repository, commitId); return getSubmodules(repository, commit.getTree()); } /** * Returns the list of submodules for this repository. * * @param repository * @param commit * @return list of submodules */ public static List getSubmodules(Repository repository, RevTree tree) { List list = new ArrayList(); byte [] blob = getByteContent(repository, tree, ".gitmodules", false); if (blob == null) { return list; } try { BlobBasedConfig config = new BlobBasedConfig(repository.getConfig(), blob); for (String module : config.getSubsections("submodule")) { String path = config.getString("submodule", module, "path"); String url = config.getString("submodule", module, "url"); list.add(new SubmoduleModel(module, path, url)); } } catch (ConfigInvalidException e) { LOGGER.error("Failed to load .gitmodules file for " + repository.getDirectory(), e); } return list; } /** * Returns the submodule definition for the specified path at the specified * commit. If no module is defined for the path, null is returned. * * @param repository * @param commit * @param path * @return a submodule definition or null if there is no submodule */ public static SubmoduleModel getSubmoduleModel(Repository repository, String commitId, String path) { for (SubmoduleModel model : getSubmodules(repository, commitId)) { if (model.path.equals(path)) { return model; } } return null; } public static String getSubmoduleCommitId(Repository repository, String path, RevCommit commit) { String commitId = null; RevWalk rw = new RevWalk(repository); TreeWalk tw = new TreeWalk(repository); tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path))); try { tw.reset(commit.getTree()); while (tw.next()) { if (tw.isSubtree() && !path.equals(tw.getPathString())) { tw.enterSubtree(); continue; } if (FileMode.GITLINK == tw.getFileMode(0)) { commitId = tw.getObjectId(0).getName(); break; } } } catch (Throwable t) { error(t, repository, "{0} can't find {1} in commit {2}", path, commit.name()); } finally { rw.dispose(); tw.close(); } return commitId; } /** * Returns the list of notes entered about the commit from the refs/notes * namespace. If the repository does not exist or is empty, an empty list is * returned. * * @param repository * @param commit * @return list of notes */ public static List getNotesOnCommit(Repository repository, RevCommit commit) { List list = new ArrayList(); if (!hasCommits(repository)) { return list; } List noteBranches = getNoteBranches(repository, true, -1); for (RefModel notesRef : noteBranches) { RevTree notesTree = JGitUtils.getCommit(repository, notesRef.getName()).getTree(); // flat notes list String notePath = commit.getName(); String text = getStringContent(repository, notesTree, notePath); if (!StringUtils.isEmpty(text)) { List history = getRevLog(repository, notesRef.getName(), notePath, 0, -1); RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history .size() - 1)); GitNote gitNote = new GitNote(noteRef, text); list.add(gitNote); continue; } // folder structure StringBuilder sb = new StringBuilder(commit.getName()); sb.insert(2, '/'); notePath = sb.toString(); text = getStringContent(repository, notesTree, notePath); if (!StringUtils.isEmpty(text)) { List history = getRevLog(repository, notesRef.getName(), notePath, 0, -1); RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history .size() - 1)); GitNote gitNote = new GitNote(noteRef, text); list.add(gitNote); } } return list; } /** * this method creates an incremental revision number as a tag according to * the amount of already existing tags, which start with a defined prefix. * * @param repository * @param objectId * @param tagger * @param prefix * @param intPattern * @param message * @return true if operation was successful, otherwise false */ public static boolean createIncrementalRevisionTag(Repository repository, String objectId, PersonIdent tagger, String prefix, String intPattern, String message) { boolean result = false; Iterator> iterator = repository.getTags().entrySet().iterator(); long lastRev = 0; while (iterator.hasNext()) { Entry entry = iterator.next(); if (entry.getKey().startsWith(prefix)) { try { long val = Long.parseLong(entry.getKey().substring(prefix.length())); if (val > lastRev) { lastRev = val; } } catch (Exception e) { // this tag is NOT an incremental revision tag } } } DecimalFormat df = new DecimalFormat(intPattern); result = createTag(repository, objectId, tagger, prefix + df.format((lastRev + 1)), message); return result; } /** * creates a tag in a repository * * @param repository * @param objectId, the ref the tag points towards * @param tagger, the person tagging the object * @param tag, the string label * @param message, the string message * @return boolean, true if operation was successful, otherwise false */ public static boolean createTag(Repository repository, String objectId, PersonIdent tagger, String tag, String message) { try { Git gitClient = Git.open(repository.getDirectory()); TagCommand tagCommand = gitClient.tag(); tagCommand.setTagger(tagger); tagCommand.setMessage(message); if (objectId != null) { RevObject revObj = getCommit(repository, objectId); tagCommand.setObjectId(revObj); } tagCommand.setName(tag); Ref call = tagCommand.call(); return call != null ? true : false; } catch (Exception e) { error(e, repository, "Failed to create tag {1} in repository {0}", objectId, tag); } return false; } /** * Create an orphaned branch in a repository. * * @param repository * @param branchName * @param author * if unspecified, Gitblit will be the author of this new branch * @return true if successful */ public static boolean createOrphanBranch(Repository repository, String branchName, PersonIdent author) { boolean success = false; String message = "Created branch " + branchName; if (author == null) { author = new PersonIdent("Gitblit", "gitblit@localhost"); } try { ObjectInserter odi = repository.newObjectInserter(); try { // Create a blob object to insert into a tree ObjectId blobId = odi.insert(Constants.OBJ_BLOB, message.getBytes(Constants.CHARACTER_ENCODING)); // Create a tree object to reference from a commit TreeFormatter tree = new TreeFormatter(); tree.append(".branch", FileMode.REGULAR_FILE, blobId); ObjectId treeId = odi.insert(tree); // Create a commit object CommitBuilder commit = new CommitBuilder(); commit.setAuthor(author); commit.setCommitter(author); commit.setEncoding(Constants.CHARACTER_ENCODING); commit.setMessage(message); commit.setTreeId(treeId); // Insert the commit into the repository ObjectId commitId = odi.insert(commit); odi.flush(); RevWalk revWalk = new RevWalk(repository); try { RevCommit revCommit = revWalk.parseCommit(commitId); if (!branchName.startsWith("refs/")) { branchName = "refs/heads/" + branchName; } RefUpdate ru = repository.updateRef(branchName); ru.setNewObjectId(commitId); ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); Result rc = ru.forceUpdate(); switch (rc) { case NEW: case FORCED: case FAST_FORWARD: success = true; break; default: success = false; } } finally { revWalk.close(); } } finally { odi.close(); } } catch (Throwable t) { error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName); } return success; } /** * Reads the sparkleshare id, if present, from the repository. * * @param repository * @return an id or null */ public static String getSparkleshareId(Repository repository) { byte[] content = getByteContent(repository, null, ".sparkleshare", false); if (content == null) { return null; } return StringUtils.decodeString(content); } /** * Automatic repair of (some) invalid refspecs. These are the result of a * bug in JGit cloning where a double forward-slash was injected. :( * * @param repository * @return true, if the refspecs were repaired */ public static boolean repairFetchSpecs(Repository repository) { StoredConfig rc = repository.getConfig(); // auto-repair broken fetch ref specs for (String name : rc.getSubsections("remote")) { int invalidSpecs = 0; int repairedSpecs = 0; List specs = new ArrayList(); for (String spec : rc.getStringList("remote", name, "fetch")) { try { RefSpec rs = new RefSpec(spec); // valid spec specs.add(spec); } catch (IllegalArgumentException e) { // invalid spec invalidSpecs++; if (spec.contains("//")) { // auto-repair this known spec bug spec = spec.replace("//", "/"); specs.add(spec); repairedSpecs++; } } } if (invalidSpecs == repairedSpecs && repairedSpecs > 0) { // the fetch specs were automatically repaired rc.setStringList("remote", name, "fetch", specs); try { rc.save(); rc.load(); LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory()); return true; } catch (Exception e) { LOGGER.error(null, e); } } else if (invalidSpecs > 0) { LOGGER.error("mirror executor found {} invalid fetch refspecs for {}", invalidSpecs, repository.getDirectory()); } } return false; } /** * Returns true if the commit identified by commitId is an ancestor or the * the commit identified by tipId. * * @param repository * @param commitId * @param tipId * @return true if there is the commit is an ancestor of the tip */ public static boolean isMergedInto(Repository repository, String commitId, String tipId) { try { return isMergedInto(repository, repository.resolve(commitId), repository.resolve(tipId)); } catch (Exception e) { LOGGER.error("Failed to determine isMergedInto", e); } return false; } /** * Returns true if the commit identified by commitId is an ancestor or the * the commit identified by tipId. * * @param repository * @param commitId * @param tipId * @return true if there is the commit is an ancestor of the tip */ public static boolean isMergedInto(Repository repository, ObjectId commitId, ObjectId tipCommitId) { // traverse the revlog looking for a commit chain between the endpoints RevWalk rw = new RevWalk(repository); try { // must re-lookup RevCommits to workaround undocumented RevWalk bug RevCommit tip = rw.lookupCommit(tipCommitId); RevCommit commit = rw.lookupCommit(commitId); return rw.isMergedInto(commit, tip); } catch (Exception e) { LOGGER.error("Failed to determine isMergedInto", e); } finally { rw.dispose(); } return false; } /** * Returns the merge base of two commits or null if there is no common * ancestry. * * @param repository * @param commitIdA * @param commitIdB * @return the commit id of the merge base or null if there is no common base */ public static String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) { RevWalk rw = new RevWalk(repository); try { RevCommit a = rw.lookupCommit(commitIdA); RevCommit b = rw.lookupCommit(commitIdB); rw.setRevFilter(RevFilter.MERGE_BASE); rw.markStart(a); rw.markStart(b); RevCommit mergeBase = rw.next(); if (mergeBase == null) { return null; } return mergeBase.getName(); } catch (Exception e) { LOGGER.error("Failed to determine merge base", e); } finally { rw.dispose(); } return null; } public static enum MergeStatus { MISSING_INTEGRATION_BRANCH, MISSING_SRC_BRANCH, NOT_MERGEABLE, FAILED, ALREADY_MERGED, MERGEABLE, MERGED; } /** * Determines if we can cleanly merge one branch into another. Returns true * if we can merge without conflict, otherwise returns false. * * @param repository * @param src * @param toBranch * @return true if we can merge without conflict */ public static MergeStatus canMerge(Repository repository, String src, String toBranch) { RevWalk revWalk = null; try { revWalk = new RevWalk(repository); ObjectId branchId = repository.resolve(toBranch); if (branchId == null) { return MergeStatus.MISSING_INTEGRATION_BRANCH; } ObjectId srcId = repository.resolve(src); if (srcId == null) { return MergeStatus.MISSING_SRC_BRANCH; } RevCommit branchTip = revWalk.lookupCommit(branchId); RevCommit srcTip = revWalk.lookupCommit(srcId); if (revWalk.isMergedInto(srcTip, branchTip)) { // already merged return MergeStatus.ALREADY_MERGED; } else if (revWalk.isMergedInto(branchTip, srcTip)) { // fast-forward return MergeStatus.MERGEABLE; } RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); boolean canMerge = merger.merge(branchTip, srcTip); if (canMerge) { return MergeStatus.MERGEABLE; } } catch (NullPointerException e) { LOGGER.error("Failed to determine canMerge", e); } catch (IOException e) { LOGGER.error("Failed to determine canMerge", e); } finally { if (revWalk != null) { revWalk.close(); } } return MergeStatus.NOT_MERGEABLE; } public static class MergeResult { public final MergeStatus status; public final String sha; MergeResult(MergeStatus status, String sha) { this.status = status; this.sha = sha; } } /** * Tries to merge a commit into a branch. If there are conflicts, the merge * will fail. * * @param repository * @param src * @param toBranch * @param committer * @param message * @return the merge result */ public static MergeResult merge(Repository repository, String src, String toBranch, PersonIdent committer, String message) { if (!toBranch.startsWith(Constants.R_REFS)) { // branch ref doesn't start with ref, assume this is a branch head toBranch = Constants.R_HEADS + toBranch; } RevWalk revWalk = null; try { revWalk = new RevWalk(repository); RevCommit branchTip = revWalk.lookupCommit(repository.resolve(toBranch)); RevCommit srcTip = revWalk.lookupCommit(repository.resolve(src)); if (revWalk.isMergedInto(srcTip, branchTip)) { // already merged return new MergeResult(MergeStatus.ALREADY_MERGED, null); } RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); boolean merged = merger.merge(branchTip, srcTip); if (merged) { // create a merge commit and a reference to track the merge commit ObjectId treeId = merger.getResultTreeId(); ObjectInserter odi = repository.newObjectInserter(); try { // Create a commit object CommitBuilder commitBuilder = new CommitBuilder(); commitBuilder.setCommitter(committer); commitBuilder.setAuthor(committer); commitBuilder.setEncoding(Constants.CHARSET); if (StringUtils.isEmpty(message)) { message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName()); } commitBuilder.setMessage(message); commitBuilder.setParentIds(branchTip.getId(), srcTip.getId()); commitBuilder.setTreeId(treeId); // Insert the merge commit into the repository ObjectId mergeCommitId = odi.insert(commitBuilder); odi.flush(); // set the merge ref to the merge commit RevCommit mergeCommit = revWalk.parseCommit(mergeCommitId); RefUpdate mergeRefUpdate = repository.updateRef(toBranch); mergeRefUpdate.setNewObjectId(mergeCommitId); mergeRefUpdate.setRefLogMessage("commit: " + mergeCommit.getShortMessage(), false); RefUpdate.Result rc = mergeRefUpdate.update(); switch (rc) { case FAST_FORWARD: // successful, clean merge break; default: throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when merging commit {1} into {2} in {3}", rc.name(), srcTip.getName(), branchTip.getName(), repository.getDirectory())); } // return the merge commit id return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName()); } finally { odi.close(); } } } catch (IOException e) { LOGGER.error("Failed to merge", e); } finally { if (revWalk != null) { revWalk.close(); } } return new MergeResult(MergeStatus.FAILED, null); } /** * Returns the LFS URL for the given oid * Currently assumes that the Gitblit Filestore is used * * @param baseURL * @param repository name * @param oid of lfs item * @return the lfs item URL */ public static String getLfsRepositoryUrl(String baseURL, String repositoryName, String oid) { if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') { baseURL = baseURL.substring(0, baseURL.length() - 1); } return baseURL + com.gitblit.Constants.R_PATH + repositoryName + "/" + com.gitblit.Constants.R_LFS + "objects/" + oid; } /** * Returns all tree entries that do not match the ignore paths. * * @param db * @param ignorePaths * @param dcBuilder * @throws IOException */ public static List getTreeEntries(Repository db, String branch, Collection ignorePaths) throws IOException { List list = new ArrayList(); TreeWalk tw = null; try { ObjectId treeId = db.resolve(branch + "^{tree}"); if (treeId == null) { // branch does not exist yet return list; } tw = new TreeWalk(db); int hIdx = tw.addTree(treeId); tw.setRecursive(true); while (tw.next()) { String path = tw.getPathString(); CanonicalTreeParser hTree = null; if (hIdx != -1) { hTree = tw.getTree(hIdx, CanonicalTreeParser.class); } if (!ignorePaths.contains(path)) { // add all other tree entries if (hTree != null) { final DirCacheEntry entry = new DirCacheEntry(path); entry.setObjectId(hTree.getEntryObjectId()); entry.setFileMode(hTree.getEntryFileMode()); list.add(entry); } } } } finally { if (tw != null) { tw.close(); } } return list; } public static boolean commitIndex(Repository db, String branch, DirCache index, ObjectId parentId, boolean forceCommit, String author, String authorEmail, String message) throws IOException, ConcurrentRefUpdateException { boolean success = false; ObjectId headId = db.resolve(branch + "^{commit}"); ObjectId baseId = parentId; if (baseId == null || headId == null) { return false; } ObjectInserter odi = db.newObjectInserter(); try { // Create the in-memory index of the new/updated ticket ObjectId indexTreeId = index.writeTree(odi); // Create a commit object PersonIdent ident = new PersonIdent(author, authorEmail); if (forceCommit == false) { ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true); merger.setObjectInserter(odi); merger.setBase(baseId); boolean mergeSuccess = merger.merge(indexTreeId, headId); if (mergeSuccess) { indexTreeId = merger.getResultTreeId(); } else { //Manual merge required return false; } } CommitBuilder commit = new CommitBuilder(); commit.setAuthor(ident); commit.setCommitter(ident); commit.setEncoding(com.gitblit.Constants.ENCODING); commit.setMessage(message); commit.setParentId(headId); commit.setTreeId(indexTreeId); // Insert the commit into the repository ObjectId commitId = odi.insert(commit); odi.flush(); RevWalk revWalk = new RevWalk(db); try { RevCommit revCommit = revWalk.parseCommit(commitId); RefUpdate ru = db.updateRef(branch); ru.setForceUpdate(forceCommit); ru.setNewObjectId(commitId); ru.setExpectedOldObjectId(headId); ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); Result rc = ru.update(); switch (rc) { case NEW: case FORCED: case FAST_FORWARD: success = true; break; case REJECTED: case LOCK_FAILURE: throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, ru.getRef(), rc); default: throw new JGitInternalException(MessageFormat.format( JGitText.get().updatingRefFailed, branch, commitId.toString(), rc)); } } finally { revWalk.close(); } } finally { odi.close(); } return success; } /** * Returns true if the commit identified by commitId is at the tip of it's branch. * * @param repository * @param commitId * @return true if the given commit is the tip */ public static boolean isTip(Repository repository, String commitId) { try { RefModel tip = getBranch(repository, commitId); return (tip != null); } catch (Exception e) { LOGGER.error("Failed to determine isTip", e); } return false; } }