From 13417cf9c6eec555b51da49742e47939d2f5715b Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Fri, 19 Oct 2012 22:47:33 -0400 Subject: [PATCH] Exclude submodules from zip downloads (issue 151) --- src/com/gitblit/utils/JGitUtils.java | 902 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 files changed, 765 insertions(+), 137 deletions(-) diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java index 03a5d00..bc44f00 100644 --- a/src/com/gitblit/utils/JGitUtils.java +++ b/src/com/gitblit/utils/JGitUtils.java @@ -20,22 +20,23 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.Charset; +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.List; import java.util.Map; import java.util.Map.Entry; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; @@ -44,23 +45,30 @@ import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.StopWalkException; +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.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.FileRepository; +import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.treewalk.TreeWalk; @@ -79,6 +87,7 @@ import com.gitblit.models.PathModel; import com.gitblit.models.PathModel.PathChangeModel; import com.gitblit.models.RefModel; +import com.gitblit.models.SubmoduleModel; /** * Collection of static methods for retrieving information from a repository. @@ -89,6 +98,29 @@ 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<Object> parameters = new ArrayList<Object>(); + 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 <email @@ -110,6 +142,15 @@ } /** + * 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. @@ -117,33 +158,64 @@ * @param repositoriesFolder * @param name * @param fromUrl - * @return FetchResult + * @return CloneResult * @throws Exception */ - public static FetchResult cloneRepository(File repositoriesFolder, String name, String fromUrl) + public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl) throws Exception { - FetchResult result = null; - if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) { - name += Constants.DOT_GIT_EXT; + 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); FileRepository repository = new FileRepository(gitDir); - result = fetchRepository(repository); + result.fetchResult = fetchRepository(credentialsProvider, repository); repository.close(); } else { CloneCommand clone = new CloneCommand(); - clone.setBare(true); + clone.setBare(bare); clone.setCloneAllBranches(true); clone.setURI(fromUrl); clone.setDirectory(folder); - clone.call(); + 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. - File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED); - FileRepository repository = new FileRepository(gitDir); - result = fetchRepository(repository); + result.createdRepository = true; + result.fetchResult = fetchRepository(credentialsProvider, repository); repository.close(); } return result; @@ -153,13 +225,14 @@ * 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(Repository repository, RefSpec... refSpecs) - throws Exception { + public static FetchResult fetchRepository(CredentialsProvider credentialsProvider, + Repository repository, RefSpec... refSpecs) throws Exception { Git git = new Git(repository); FetchCommand fetch = git.fetch(); List<RefSpec> specs = new ArrayList<RefSpec>(); @@ -170,9 +243,12 @@ } else { specs.addAll(Arrays.asList(refSpecs)); } + if (credentialsProvider != null) { + fetch.setCredentialsProvider(credentialsProvider); + } fetch.setRefSpecs(specs); - FetchResult result = fetch.call(); - return result; + FetchResult fetchRes = fetch.call(); + return fetchRes; } /** @@ -183,30 +259,44 @@ * @return Repository */ public static Repository createRepository(File repositoriesFolder, String name) { - Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call(); - return git.getRepository(); + try { + Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call(); + return git.getRepository(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } } /** * Returns a list of repository names in the specified folder. * * @param repositoriesFolder - * @param exportAll - * if true, all repositories are listed. If false only the - * repositories with a "git-daemon-export-ok" file are included + * @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<String> getRepositoryList(File repositoriesFolder, boolean exportAll, - boolean searchSubfolders) { + public static List<String> getRepositoryList(File repositoriesFolder, boolean onlyBare, + boolean searchSubfolders, int depth, List<String> exclusions) { List<String> list = new ArrayList<String>(); if (repositoriesFolder == null || !repositoriesFolder.exists()) { return list; } + List<Pattern> patterns = new ArrayList<Pattern>(); + if (!ArrayUtils.isEmpty(exclusions)) { + for (String regex : exclusions) { + patterns.add(Pattern.compile(regex)); + } + } list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder, - exportAll, searchSubfolders)); - Collections.sort(list); + onlyBare, searchSubfolders, depth, patterns)); + StringUtils.sortRepositorynames(list); return list; } @@ -217,33 +307,60 @@ * basePath is stripped from the repository name as repositories * are relative to this path * @param searchFolder - * @param exportAll - * if true all repositories are listed. If false only the - * repositories with a "git-daemon-export-ok" file are included + * @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<String> getRepositoryList(String basePath, File searchFolder, - boolean exportAll, boolean searchSubfolders) { + boolean onlyBare, boolean searchSubfolders, int depth, List<Pattern> patterns) { + File baseFile = new File(basePath); List<String> list = new ArrayList<String>(); + 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) { - boolean exportRepository = exportAll - || new File(gitDir, "git-daemon-export-ok").exists(); - - if (!exportRepository) { + if (onlyBare && gitDir.getName().equals(".git")) { continue; } - // determine repository name relative to base path - String repository = StringUtils.getRelativePath(basePath, - file.getAbsolutePath()); - list.add(repository); - } else if (searchSubfolders) { + 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, exportAll, searchSubfolders)); + list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders, + nextDepth, patterns)); } } } @@ -263,19 +380,24 @@ if (!hasCommits(repository)) { return null; } - if (StringUtils.isEmpty(branch)) { - branch = Constants.HEAD; - } 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(repository.resolve(branch)); + RevCommit head = walk.parseCommit(branchObject); walk.markStart(head); commit = walk.next(); walk.dispose(); } catch (Throwable t) { - LOGGER.error("Failed to determine first commit", t); + error(t, repository, "{0} failed to determine first commit"); } return commit; } @@ -310,7 +432,7 @@ * @return true if the repository has commits */ public static boolean hasCommits(Repository repository) { - if (repository != null && repository.getDirectory().exists()) { + if (repository != null && repository.getDirectory().exists()) { return (new File(repository.getDirectory(), "objects").list().length > 2) || (new File(repository.getDirectory(), "objects/pack").list().length > 0); } @@ -323,11 +445,9 @@ * last modified date of the repository folder is returned. * * @param repository - * @param branch - * if unspecified, HEAD is assumed. * @return */ - public static Date getLastChange(Repository repository, String branch) { + public static Date getLastChange(Repository repository) { if (!hasCommits(repository)) { // null repository if (repository == null) { @@ -336,21 +456,47 @@ // fresh repository return new Date(repository.getDirectory().lastModified()); } - if (StringUtils.isEmpty(branch)) { - branch = Constants.HEAD; + + List<RefModel> branchModels = getLocalBranches(repository, true, -1); + if (branchModels.size() > 0) { + // find most recent branch update + Date lastChange = new Date(0); + for (RefModel branchModel : branchModels) { + if (branchModel.getDate().after(lastChange)) { + lastChange = branchModel.getDate(); + } + } + return lastChange; } - RevCommit commit = getCommit(repository, branch); - return getCommitDate(commit); + + // default to the repository folder modification date + return new Date(repository.getDirectory().lastModified()); } /** * Retrieves a Java Date from a Git commit. * * @param commit - * @return date of the 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); + } + return commit.getAuthorIdent().getWhen(); } /** @@ -368,16 +514,19 @@ } RevCommit commit = null; try { + // resolve object id + ObjectId branchObject; if (StringUtils.isEmpty(objectId)) { - objectId = Constants.HEAD; + branchObject = getDefaultBranch(repository); + } else { + branchObject = repository.resolve(objectId); } - ObjectId object = repository.resolve(objectId); RevWalk walk = new RevWalk(repository); - RevCommit rev = walk.parseCommit(object); + RevCommit rev = walk.parseCommit(branchObject); commit = rev; walk.dispose(); } catch (Throwable t) { - LOGGER.error("Failed to get commit " + objectId, t); + error(t, repository, "{0} failed to get commit {1}", objectId); } return commit; } @@ -398,7 +547,7 @@ byte[] content = null; try { if (tree == null) { - ObjectId object = repository.resolve(Constants.HEAD); + ObjectId object = getDefaultBranch(repository); RevCommit commit = rw.parseCommit(object); tree = commit.getTree(); } @@ -410,21 +559,23 @@ } ObjectId entid = tw.getObjectId(0); FileMode entmode = tw.getFileMode(0); - RevObject ro = rw.lookupAny(entid, entmode.getObjectType()); - rw.parseBody(ro); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - ObjectLoader ldr = repository.open(ro.getId(), Constants.OBJ_BLOB); - byte[] tmp = new byte[4096]; - InputStream in = ldr.openStream(); - int n; - while ((n = in.read(tmp)) > 0) { - os.write(tmp, 0, n); + if (entmode != FileMode.GITLINK) { + RevObject ro = rw.lookupAny(entid, entmode.getObjectType()); + rw.parseBody(ro); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ObjectLoader ldr = repository.open(ro.getId(), Constants.OBJ_BLOB); + byte[] tmp = new byte[4096]; + InputStream in = ldr.openStream(); + int n; + while ((n = in.read(tmp)) > 0) { + os.write(tmp, 0, n); + } + in.close(); + content = os.toByteArray(); } - in.close(); - content = os.toByteArray(); } } catch (Throwable t) { - LOGGER.error("Can't find " + path + " in tree " + tree.name(), t); + error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name()); } finally { rw.dispose(); tw.release(); @@ -439,14 +590,15 @@ * @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) { + public static String getStringContent(Repository repository, RevTree tree, String blobPath, String... charsets) { byte[] content = getByteContent(repository, tree, blobPath); if (content == null) { return null; } - return new String(content, Charset.forName(Constants.CHARACTER_ENCODING)); + return StringUtils.decodeString(content, charsets); } /** @@ -473,7 +625,7 @@ in.close(); content = os.toByteArray(); } catch (Throwable t) { - LOGGER.error("Can't find blob " + objectId, t); + error(t, repository, "{0} can't find blob {1}", objectId); } finally { rw.dispose(); } @@ -485,14 +637,15 @@ * * @param repository * @param objectId + * @param charsets optional * @return UTF-8 string content */ - public static String getStringContent(Repository repository, String objectId) { + public static String getStringContent(Repository repository, String objectId, String... charsets) { byte[] content = getByteContent(repository, objectId); if (content == null) { return null; } - return new String(content, Charset.forName(Constants.CHARACTER_ENCODING)); + return StringUtils.decodeString(content, charsets); } /** @@ -514,7 +667,7 @@ return list; } if (commit == null) { - commit = getCommit(repository, Constants.HEAD); + commit = getCommit(repository, null); } final TreeWalk tw = new TreeWalk(repository); try { @@ -543,7 +696,7 @@ } } } catch (IOException e) { - LOGGER.error("Failed to get files for commit " + commit.getName(), e); + error(e, repository, "{0} failed to get files for commit {1}", commit.getName()); } finally { tw.release(); } @@ -568,7 +721,7 @@ RevWalk rw = new RevWalk(repository); try { if (commit == null) { - ObjectId object = repository.resolve(Constants.HEAD); + ObjectId object = getDefaultBranch(repository); commit = rw.parseCommit(object); } @@ -579,7 +732,8 @@ tw.addTree(commit.getTree()); while (tw.next()) { list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw - .getRawMode(0), commit.getId().getName(), ChangeType.ADD)); + .getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(), + ChangeType.ADD)); } tw.release(); } else { @@ -590,19 +744,28 @@ df.setDetectRenames(true); List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree()); for (DiffEntry diff : diffs) { + String objectId = null; + if (FileMode.GITLINK.equals(diff.getNewMode())) { + objectId = diff.getNewId().name(); + } + if (diff.getChangeType().equals(ChangeType.DELETE)) { list.add(new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff - .getNewMode().getBits(), commit.getId().getName(), 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(), commit.getId().getName(), diff + .getNewMode().getBits(), objectId, commit.getId().getName(), diff .getChangeType())); } } } } catch (Throwable t) { - LOGGER.error("failed to determine files in commit!", t); + error(t, repository, "{0} failed to determine files in commit!"); } finally { rw.dispose(); } @@ -610,25 +773,40 @@ } /** - * Returns the list of files in the repository 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. + * 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<PathModel> getDocuments(Repository repository, List<String> 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<PathModel> getDocuments(Repository repository, List<String> extensions, + String objectId) { List<PathModel> list = new ArrayList<PathModel>(); if (!hasCommits(repository)) { return list; } - RevCommit commit = getCommit(repository, Constants.HEAD); + RevCommit commit = getCommit(repository, objectId); final TreeWalk tw = new TreeWalk(repository); try { tw.addTree(commit.getTree()); if (extensions != null && extensions.size() > 0) { - Collection<TreeFilter> suffixFilters = new ArrayList<TreeFilter>(); + List<TreeFilter> suffixFilters = new ArrayList<TreeFilter>(); for (String extension : extensions) { if (extension.charAt(0) == '.') { suffixFilters.add(PathSuffixFilter.create("\\" + extension)); @@ -637,7 +815,12 @@ suffixFilters.add(PathSuffixFilter.create("\\." + extension)); } } - TreeFilter filter = OrTreeFilter.create(suffixFilters); + TreeFilter filter; + if (suffixFilters.size() == 1) { + filter = suffixFilters.get(0); + } else { + filter = OrTreeFilter.create(suffixFilters); + } tw.setFilter(filter); tw.setRecursive(true); } @@ -645,7 +828,7 @@ list.add(getPathModel(tw, null, commit)); } } catch (IOException e) { - LOGGER.error("Failed to get documents for commit " + commit.getName(), e); + error(e, repository, "{0} failed to get documents for commit {1}", commit.getName()); } finally { tw.release(); } @@ -669,15 +852,16 @@ } else { name = tw.getPathString().substring(basePath.length() + 1); } + ObjectId objectId = tw.getObjectId(0); try { - if (!tw.isSubtree()) { - size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB); + if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) { + size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB); } } catch (Throwable t) { - LOGGER.error("Failed to retrieve blob size", t); + error(t, null, "failed to retrieve blob size for " + tw.getPathString()); } return new PathModel(name, tw.getPathString(), size, tw.getFileMode(0).getBits(), - commit.getName()); + objectId.getName(), commit.getName()); } /** @@ -694,14 +878,50 @@ } else if (FileMode.EXECUTABLE_FILE.equals(mode)) { return "-rwxr-xr-x"; } else if (FileMode.SYMLINK.equals(mode)) { - // FIXME symlink permissions return "symlink"; } else if (FileMode.GITLINK.equals(mode)) { - // FIXME gitlink permissions - return "gitlink"; + return "submodule"; } - // FIXME missing permissions 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<RevCommit> getRevLog(Repository repository, String objectId, Date minimumDate) { + List<RevCommit> list = new ArrayList<RevCommit>(); + 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<RevCommit> 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; } /** @@ -713,7 +933,7 @@ * @return list of commits */ public static List<RevCommit> getRevLog(Repository repository, int maxCount) { - return getRevLog(repository, Constants.HEAD, 0, maxCount); + return getRevLog(repository, null, 0, maxCount); } /** @@ -762,12 +982,19 @@ return list; } try { + // resolve branch + ObjectId branchObject; if (StringUtils.isEmpty(objectId)) { - objectId = Constants.HEAD; + branchObject = getDefaultBranch(repository); + } else { + branchObject = repository.resolve(objectId); } + if (branchObject == null) { + return list; + } + RevWalk rw = new RevWalk(repository); - ObjectId object = repository.resolve(objectId); - rw.markStart(rw.parseCommit(object)); + rw.markStart(rw.parseCommit(branchObject)); if (!StringUtils.isEmpty(path)) { TreeFilter filter = AndTreeFilter.create( PathFilterGroup.createFromStrings(Collections.singleton(path)), @@ -796,30 +1023,53 @@ } rw.dispose(); } catch (Throwable t) { - LOGGER.error("Failed to get revlog", t); + error(t, repository, "{0} failed to get {1} revlog for path {2}", objectId, path); } return list; } /** - * Enumeration of the search types. + * 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 enum SearchType { - AUTHOR, COMMITTER, COMMIT; + public static List<RevCommit> getRevLog(Repository repository, String startRangeId, + String endRangeId) { + List<RevCommit> list = new ArrayList<RevCommit>(); + if (!hasCommits(repository)) { + return list; + } + try { + ObjectId endRange = repository.resolve(endRangeId); + ObjectId startRange = repository.resolve(startRangeId); - public static SearchType forName(String name) { - for (SearchType type : values()) { - if (type.name().equalsIgnoreCase(name)) { - return type; - } + 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)); } - return COMMIT; - } - @Override - public String toString() { - return name().toLowerCase(); + Iterable<RevCommit> 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; } /** @@ -840,7 +1090,7 @@ * @return matching list of commits */ public static List<RevCommit> searchRevlogs(Repository repository, String objectId, - String value, final SearchType type, int offset, int maxCount) { + String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) { final String lcValue = value.toLowerCase(); List<RevCommit> list = new ArrayList<RevCommit>(); if (maxCount == 0) { @@ -850,9 +1100,14 @@ return list; } try { + // resolve branch + ObjectId branchObject; if (StringUtils.isEmpty(objectId)) { - objectId = Constants.HEAD; + branchObject = getDefaultBranch(repository); + } else { + branchObject = repository.resolve(objectId); } + RevWalk rw = new RevWalk(repository); rw.setRevFilter(new RevFilter() { @@ -887,8 +1142,7 @@ } }); - ObjectId object = repository.resolve(objectId); - rw.markStart(rw.parseCommit(object)); + rw.markStart(rw.parseCommit(branchObject)); Iterable<RevCommit> revlog = rw; if (offset > 0) { int count = 0; @@ -911,9 +1165,200 @@ } rw.dispose(); } catch (Throwable t) { - LOGGER.error("Failed to search revlogs", 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<RefModel> 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(); + if (!target.startsWith(Constants.R_HEADS)) { + // refers to an actual commit, probably a tag + // find latest tag that matches the commit, if any + List<RefModel> tagModels = getTags(repository, true, -1); + if (tagModels.size() > 0) { + RefModel tag = null; + Date lastDate = new Date(0); + for (RefModel tagModel : tagModels) { + if (tagModel.getReferencedObjectId().getName().equals(target) && + tagModel.getDate().after(lastDate)) { + tag = tagModel; + lastDate = tag.getDate(); + } + } + target = tag.getName(); + } + } + } 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_HEADS)) { + 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) { + String branchName = branch; + if (!branchName.startsWith(Constants.R_HEADS)) { + branchName = Constants.R_HEADS + branch; + } + + try { + RefUpdate refUpdate = repository.updateRef(branchName, 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(), branchName, result)); + } + } catch (Throwable t) { + error(t, repository, "{0} failed to delete {1}", branchName); + } + 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<String> getAvailableHeadTargets(Repository repository) { + List<String> targets = new ArrayList<String>(); + 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; } /** @@ -923,9 +1368,23 @@ * @return all refs grouped by their referenced object id */ public static Map<ObjectId, List<RefModel>> 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<ObjectId, List<RefModel>> getAllRefs(Repository repository, boolean includeRemoteRefs) { List<RefModel> list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1); Map<ObjectId, List<RefModel>> refs = new HashMap<ObjectId, List<RefModel>>(); 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<RefModel>()); @@ -1044,9 +1503,109 @@ list = new ArrayList<RefModel>(list.subList(0, maxCount)); } } catch (IOException e) { - LOGGER.error("Failed to retrieve " + refs, 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.displayName.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.displayName.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<SubmoduleModel> 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<SubmoduleModel> getSubmodules(Repository repository, RevTree tree) { + List<SubmoduleModel> list = new ArrayList<SubmoduleModel>(); + byte [] blob = getByteContent(repository, tree, ".gitmodules"); + 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; } /** @@ -1066,10 +1625,23 @@ List<RefModel> 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<RevCommit> 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, '/'); - String notePath = sb.toString(); - String text = getStringContent(repository, notesTree, notePath); + notePath = sb.toString(); + text = getStringContent(repository, notesTree, notePath); if (!StringUtils.isEmpty(text)) { List<RevCommit> history = getRevLog(repository, notesRef.getName(), notePath, 0, -1); RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history @@ -1082,21 +1654,74 @@ } /** - * Returns a StoredConfig object for the repository. + * Create an orphaned branch in a repository. * * @param repository - * @return the StoredConfig of the repository + * @param branchName + * @param author + * if unspecified, Gitblit will be the author of this new branch + * @return true if successful */ - public static StoredConfig readConfig(Repository repository) { - StoredConfig c = repository.getConfig(); - try { - c.load(); - } catch (ConfigInvalidException cex) { - LOGGER.error("Repository configuration is invalid!", cex); - } catch (IOException cex) { - LOGGER.error("Could not open repository configuration!", cex); + 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"); } - return c; + 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.release(); + } + } finally { + odi.release(); + } + } catch (Throwable t) { + error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName); + } + return success; } /** @@ -1131,6 +1756,9 @@ } tw.setRecursive(true); while (tw.next()) { + if (tw.getFileMode(0) == FileMode.GITLINK) { + continue; + } ZipEntry entry = new ZipEntry(tw.getPathString()); entry.setSize(tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB)); @@ -1154,7 +1782,7 @@ zos.finish(); success = true; } catch (IOException e) { - LOGGER.error("Failed to zip files from commit " + commit.getName(), e); + error(e, repository, "{0} failed to zip files from commit {1}", commit.getName()); } finally { tw.release(); rw.dispose(); -- Gitblit v1.9.1