blob: 0eea1d6156b4eb701bd3a56f6ffcd4fdcaf721cf [file] [log] [blame]
/*
* 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.Set;
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.AmbiguousObjectException;
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.RevisionSyntaxException;
import org.eclipse.jgit.errors.StopWalkException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
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.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.MergeType;
import com.gitblit.GitBlitException;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.git.PatchsetCommand;
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.TicketModel.TicketAction;
import com.gitblit.models.TicketModel.TicketLink;
import com.gitblit.models.RefModel;
import com.gitblit.models.SubmoduleModel;
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<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
* address>". If the email address is empty, just "Real Name" is returned.
*
* @param person
* @return "Real Name <email address>" 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<RefSpec> specs = new ArrayList<RefSpec>();
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<File> iter = org.apache.commons.io.FileUtils.iterateFilesAndDirs(repo.getDirectory(),
TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
// Adjust permissions on file/directory
while (iter.hasNext()) {
adjustSharedPerm(iter.next(), sharedRepository);
}
}
}
return repo;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private enum GitConfigSharedRepositoryValue
{
UMASK("0", 0), FALSE("0", 0), OFF("0", 0), NO("0", 0),
GROUP("1", 0660), TRUE("1", 0660), ON("1", 0660), YES("1", 0660),
ALL("2", 0664), WORLD("2", 0664), EVERYBODY("2", 0664),
Oxxx(null, -1);
private String configValue;
private int permValue;
private GitConfigSharedRepositoryValue(String config, int perm) { configValue = config; permValue = perm; };
public String getConfigValue() { return configValue; };
public int getPerm() { return permValue; };
}
private static class GitConfigSharedRepository
{
private int intValue;
private GitConfigSharedRepositoryValue enumValue;
GitConfigSharedRepository(String s) {
if ( s == null || s.trim().isEmpty() ) {
enumValue = GitConfigSharedRepositoryValue.GROUP;
}
else {
try {
// Try one of the string values
enumValue = GitConfigSharedRepositoryValue.valueOf(s.trim().toUpperCase());
} catch (IllegalArgumentException iae) {
try {
// Try if this is an octal number
int i = Integer.parseInt(s, 8);
if ( (i & 0600) != 0600 ) {
String msg = String.format("Problem with core.sharedRepository filemode value (0%03o).\nThe owner of files must always have read and write permissions.", i);
throw new IllegalArgumentException(msg);
}
intValue = i & 0666;
enumValue = GitConfigSharedRepositoryValue.Oxxx;
} catch (NumberFormatException nfe) {
throw new IllegalArgumentException("Bad configuration value for 'shared': '" + s + "'");
}
}
}
}
String getValue() {
if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) {
if (intValue == 0) return "0";
return String.format("0%o", intValue);
}
return enumValue.getConfigValue();
}
int getPerm() {
if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) return intValue;
return enumValue.getPerm();
}
boolean isCustom() {
return enumValue == GitConfigSharedRepositoryValue.Oxxx;
}
boolean isShared() {
return (enumValue.getPerm() > 0) || enumValue == GitConfigSharedRepositoryValue.Oxxx;
}
}
/**
* Adjust file permissions of a file/directory for shared repositories
*
* @param path
* File that should get its permissions changed.
* @param configShared
* Configuration string value for the shared mode.
* @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned.
*/
public static int adjustSharedPerm(File path, String configShared) {
return adjustSharedPerm(path, new GitConfigSharedRepository(configShared));
}
/**
* Adjust file permissions of a file/directory for shared repositories
*
* @param path
* File that should get its permissions changed.
* @param configShared
* Configuration setting for the shared mode.
* @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned.
*/
public static int adjustSharedPerm(File path, GitConfigSharedRepository configShared) {
if (! configShared.isShared()) return 0;
if (! path.exists()) return -1;
int perm = configShared.getPerm();
JnaUtils.Filestat stat = JnaUtils.getFilestat(path);
if (stat == null) return -1;
int mode = stat.mode;
if (mode < 0) return -1;
// Now, here is the kicker: Under Linux, chmod'ing a sgid file whose guid is different from the process'
// effective guid will reset the sgid flag of the file. Since there is no way to get the sgid flag back in
// that case, we decide to rather not touch is and getting the right permissions will have to be achieved
// in a different way, e.g. by using an appropriate umask for the Gitblit process.
if (System.getProperty("os.name").toLowerCase().startsWith("linux")) {
if ( ((mode & (JnaUtils.S_ISGID | JnaUtils.S_ISUID)) != 0)
&& stat.gid != JnaUtils.getegid() ) {
LOGGER.debug("Not adjusting permissions to prevent clearing suid/sgid bits for '" + path + "'" );
return 0;
}
}
// If the owner has no write access, delete it from group and other, too.
if ((mode & JnaUtils.S_IWUSR) == 0) perm &= ~0222;
// If the owner has execute access, set it for all blocks that have read access.
if ((mode & JnaUtils.S_IXUSR) == JnaUtils.S_IXUSR) perm |= (perm & 0444) >> 2;
if (configShared.isCustom()) {
// Use the custom value for access permissions.
mode = (mode & ~0777) | perm;
}
else {
// Just add necessary bits to existing permissions.
mode |= perm;
}
if (path.isDirectory()) {
mode |= (mode & 0444) >> 2;
mode |= JnaUtils.S_ISGID;
}
return JnaUtils.setFilemode(path, mode);
}
/**
* Returns a list of repository names in the specified folder.
*
* @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<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,
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<String> getRepositoryList(String basePath, File searchFolder,
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) {
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<RefModel> 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<PathModel> getFilesInPath(Repository repository, String path,
RevCommit commit) {
List<PathModel> list = new ArrayList<PathModel>();
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<PathModel> getFilesInPath2(Repository repository, String path, RevCommit commit) {
List<PathModel> list = new ArrayList<PathModel>();
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<String> 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<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit) {
return getFilesInCommit(repository, commit, true);
}
/**
* Returns the list of files changed in a specified commit. If the
* repository does not exist or is empty, an empty list is returned.
*
* @param repository
* @param commit
* if null, HEAD is assumed.
* @param calculateDiffStat
* if true, each PathChangeModel will have insertions/deletions
* @return list of files changed in a commit
*/
public static List<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) {
List<PathChangeModel> list = new ArrayList<PathChangeModel>();
if (!hasCommits(repository)) {
return list;
}
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<DiffEntry> 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<PathChangeModel> getFilesInRange(Repository repository, String startCommit, String endCommit) {
List<PathChangeModel> list = new ArrayList<PathChangeModel>();
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<PathChangeModel> getFilesInRange(Repository repository, RevCommit startCommit, RevCommit endCommit) {
List<PathChangeModel> list = new ArrayList<PathChangeModel>();
if (!hasCommits(repository)) {
return list;
}
try {
DiffFormatter df = new DiffFormatter(null);
df.setRepository(repository);
df.setDiffComparator(RawTextComparator.DEFAULT);
df.setDetectRenames(true);
List<DiffEntry> 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<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, objectId);
final TreeWalk tw = new TreeWalk(repository);
try {
tw.addTree(commit.getTree());
if (extensions != null && extensions.size() > 0) {
List<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();
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<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;
}
/**
* 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<RevCommit> 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<RevCommit> 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<RevCommit> getRevLog(Repository repository, String objectId, String path,
int offset, int maxCount) {
List<RevCommit> list = new ArrayList<RevCommit>();
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<RevCommit> 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<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);
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<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;
}
/**
* 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<RevCommit> searchRevlogs(Repository repository, String objectId,
String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) {
List<RevCommit> list = new ArrayList<RevCommit>();
if (StringUtils.isEmpty(value)) {
return list;
}
if (maxCount == 0) {
return list;
}
if (!hasCommits(repository)) {
return list;
}
final String lcValue = value.toLowerCase();
try {
// resolve branch
ObjectId branchObject;
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<RevCommit> 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<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();
} 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<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;
}
/**
* Returns all refs grouped by their associated object id.
*
* @param repository
* @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>());
}
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<RefModel> 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<RefModel> 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<RefModel> 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<RefModel> 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<RefModel> 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<RefModel> 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<RefModel> 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<RefModel> getRefs(Repository repository, String refs, boolean fullName,
int maxCount, int offset) {
List<RefModel> list = new ArrayList<RefModel>();
if (maxCount == 0) {
return list;
}
if (!hasCommits(repository)) {
return list;
}
try {
Map<String, Ref> map = repository.getRefDatabase().getRefs(refs);
RevWalk rw = new RevWalk(repository);
for (Entry<String, Ref> 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<RefModel>(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<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", 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<GitNote> getNotesOnCommit(Repository repository, RevCommit commit) {
List<GitNote> list = new ArrayList<GitNote>();
if (!hasCommits(repository)) {
return list;
}
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, '/');
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
.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<Entry<String, Ref>> iterator = repository.getTags().entrySet().iterator();
long lastRev = 0;
while (iterator.hasNext()) {
Entry<String, Ref> 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<String> specs = new ArrayList<String>();
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
* @param mergeType
* Defines the integration strategy to use for merging.
* @return true if we can merge without conflict
*/
public static MergeStatus canMerge(Repository repository, String src, String toBranch, MergeType mergeType) {
IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch);
return strategy.canMerge();
}
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 mergeType
* Defines the integration strategy to use for merging.
* @param committer
* @param message
* @return the merge result
*/
public static MergeResult merge(Repository repository, String src, String toBranch, MergeType mergeType,
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;
}
IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch);
MergeResult mergeResult = strategy.merge(committer, message);
if (mergeResult.status != MergeStatus.MERGED) {
return mergeResult;
}
try {
// Update the integration branch ref
RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
mergeRefUpdate.setNewObjectId(strategy.getMergeCommit());
mergeRefUpdate.setRefLogMessage(strategy.getRefLogMessage(), false);
mergeRefUpdate.setExpectedOldObjectId(strategy.branchTip);
RefUpdate.Result rc = mergeRefUpdate.update();
switch (rc) {
case FAST_FORWARD:
// successful, clean merge
break;
default:
mergeResult = new MergeResult(MergeStatus.FAILED, null);
throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when {1} in {2}",
rc.name(), strategy.getOperationMessage(), repository.getDirectory()));
}
} catch (IOException e) {
LOGGER.error("Failed to merge", e);
}
return mergeResult;
}
private static abstract class IntegrationStrategy {
Repository repository;
String src;
String toBranch;
RevWalk revWalk;
RevCommit branchTip;
RevCommit srcTip;
RevCommit mergeCommit;
String refLogMessage;
String operationMessage;
RevCommit getMergeCommit() {
return mergeCommit;
}
String getRefLogMessage() {
return refLogMessage;
}
String getOperationMessage() {
return operationMessage;
}
IntegrationStrategy(Repository repository, String src, String toBranch) {
this.repository = repository;
this.src = src;
this.toBranch = toBranch;
}
void prepare() throws IOException {
if (revWalk == null) revWalk = new RevWalk(repository);
ObjectId branchId = repository.resolve(toBranch);
if (branchId != null) {
branchTip = revWalk.lookupCommit(branchId);
}
ObjectId srcId = repository.resolve(src);
if (srcId != null) {
srcTip = revWalk.lookupCommit(srcId);
}
}
abstract MergeStatus _canMerge() throws IOException;
MergeStatus canMerge() {
try {
prepare();
if (branchTip == null) {
return MergeStatus.MISSING_INTEGRATION_BRANCH;
}
if (srcTip == null) {
return MergeStatus.MISSING_SRC_BRANCH;
}
if (revWalk.isMergedInto(srcTip, branchTip)) {
// already merged
return MergeStatus.ALREADY_MERGED;
}
// determined by specific integration strategy
return _canMerge();
} 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;
}
abstract MergeResult _merge(PersonIdent committer, String message) throws IOException;
MergeResult merge(PersonIdent committer, String message) {
try {
prepare();
if (revWalk.isMergedInto(srcTip, branchTip)) {
// already merged
return new MergeResult(MergeStatus.ALREADY_MERGED, null);
}
// determined by specific integration strategy
return _merge(committer, message);
} catch (IOException e) {
LOGGER.error("Failed to merge", e);
} finally {
if (revWalk != null) {
revWalk.close();
}
}
return new MergeResult(MergeStatus.FAILED, null);
}
}
private static class FastForwardOnly extends IntegrationStrategy {
FastForwardOnly(Repository repository, String src, String toBranch) {
super(repository, src, toBranch);
}
@Override
MergeStatus _canMerge() throws IOException {
if (revWalk.isMergedInto(branchTip, srcTip)) {
// fast-forward
return MergeStatus.MERGEABLE;
}
return MergeStatus.NOT_MERGEABLE;
}
@Override
MergeResult _merge(PersonIdent committer, String message) throws IOException {
if (! revWalk.isMergedInto(branchTip, srcTip)) {
// is not fast-forward
return new MergeResult(MergeStatus.FAILED, null);
}
mergeCommit = srcTip;
refLogMessage = "merge " + src + ": Fast-forward";
operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", srcTip.getName(), branchTip.getName());
return new MergeResult(MergeStatus.MERGED, srcTip.getName());
}
}
private static class MergeIfNecessary extends IntegrationStrategy {
MergeIfNecessary(Repository repository, String src, String toBranch) {
super(repository, src, toBranch);
}
@Override
MergeStatus _canMerge() throws IOException {
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;
}
return MergeStatus.NOT_MERGEABLE;
}
@Override
MergeResult _merge(PersonIdent committer, String message) throws IOException {
if (revWalk.isMergedInto(branchTip, srcTip)) {
// fast-forward
mergeCommit = srcTip;
refLogMessage = "merge " + src + ": Fast-forward";
operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", branchTip.getName(), srcTip.getName());
return new MergeResult(MergeStatus.MERGED, srcTip.getName());
}
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();
mergeCommit = revWalk.parseCommit(mergeCommitId);
refLogMessage = "commit: " + mergeCommit.getShortMessage();
operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName());
// return the merge commit id
return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
} finally {
odi.close();
}
}
return new MergeResult(MergeStatus.FAILED, null);
}
}
private static class MergeAlways extends IntegrationStrategy {
MergeAlways(Repository repository, String src, String toBranch) {
super(repository, src, toBranch);
}
@Override
MergeStatus _canMerge() throws IOException {
RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
boolean canMerge = merger.merge(branchTip, srcTip);
if (canMerge) {
return MergeStatus.MERGEABLE;
}
return MergeStatus.NOT_MERGEABLE;
}
@Override
MergeResult _merge(PersonIdent committer, String message) throws IOException {
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();
mergeCommit = revWalk.parseCommit(mergeCommitId);
refLogMessage = "commit: " + mergeCommit.getShortMessage();
operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName());
// return the merge commit id
return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
} finally {
odi.close();
}
}
return new MergeResult(MergeStatus.FAILED, null);
}
}
private static class IntegrationStrategyFactory {
static IntegrationStrategy create(MergeType mergeType, Repository repository, String src, String toBranch) {
switch(mergeType) {
case FAST_FORWARD_ONLY:
return new FastForwardOnly(repository, src, toBranch);
case MERGE_IF_NECESSARY:
return new MergeIfNecessary(repository, src, toBranch);
case MERGE_ALWAYS:
return new MergeAlways(repository, src, toBranch);
}
return 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<DirCacheEntry> getTreeEntries(Repository db, String branch, Collection<String> ignorePaths) throws IOException {
List<DirCacheEntry> list = new ArrayList<DirCacheEntry>();
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;
}
/*
* Identify ticket by considering the branch the commit is on
*
* @param repository
* @param commit
* @return ticket number, or 0 if no ticket
*/
public static long getTicketNumberFromCommitBranch(Repository repository, RevCommit commit) {
// try lookup by change ref
Map<AnyObjectId, Set<Ref>> map = repository.getAllRefsByPeeledObjectId();
Set<Ref> refs = map.get(commit.getId());
if (!ArrayUtils.isEmpty(refs)) {
for (Ref ref : refs) {
long number = PatchsetCommand.getTicketNumber(ref.getName());
if (number > 0) {
return number;
}
}
}
return 0;
}
/**
* Try to identify all referenced tickets from the commit.
*
* @param commit
* @return a collection of TicketLinks
*/
@NotNull
public static List<TicketLink> identifyTicketsFromCommitMessage(Repository repository, IStoredSettings settings,
RevCommit commit) {
List<TicketLink> ticketLinks = new ArrayList<TicketLink>();
List<Long> linkedTickets = new ArrayList<Long>();
// parse commit message looking for fixes/closes #n
final String xFixDefault = "(?:fixes|closes)[\\s-]+#?(\\d+)";
String xFix = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, xFixDefault);
if (StringUtils.isEmpty(xFix)) {
xFix = xFixDefault;
}
try {
Pattern p = Pattern.compile(xFix, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(commit.getFullMessage());
while (m.find()) {
String val = m.group(1);
long number = Long.parseLong(val);
if (number > 0) {
ticketLinks.add(new TicketLink(number, TicketAction.Close));
linkedTickets.add(number);
}
}
} catch (Exception e) {
LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xFix, commit.getName()), e);
}
// parse commit message looking for ref #n
final String xRefDefault = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)";
String xRef = settings.getString(Keys.tickets.linkOnPushCommitMessageRegex, xRefDefault);
if (StringUtils.isEmpty(xRef)) {
xRef = xRefDefault;
}
try {
Pattern p = Pattern.compile(xRef, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(commit.getFullMessage());
while (m.find()) {
String val = m.group(1);
long number = Long.parseLong(val);
//Most generic case so don't included tickets more precisely linked
if ((number > 0) && (!linkedTickets.contains(number))) {
ticketLinks.add( new TicketLink(number, TicketAction.Commit, commit.getName()));
linkedTickets.add(number);
}
}
} catch (Exception e) {
LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xRef, commit.getName()), e);
}
return ticketLinks;
}
/**
* Try to identify all referenced tickets between two commits
*
* @param commit
* @param parseMessage
* @param currentTicketId, or 0 if not on a ticket branch
* @return a collection of TicketLink, or null if commit is already linked
*/
public static List<TicketLink> identifyTicketsBetweenCommits(Repository repository, IStoredSettings settings,
String baseSha, String tipSha) {
List<TicketLink> links = new ArrayList<TicketLink>();
if (repository == null) { return links; }
RevWalk walk = new RevWalk(repository);
walk.sort(RevSort.TOPO);
walk.sort(RevSort.REVERSE, true);
try {
RevCommit tip = walk.parseCommit(repository.resolve(tipSha));
RevCommit base = walk.parseCommit(repository.resolve(baseSha));
walk.markStart(tip);
walk.markUninteresting(base);
for (;;) {
RevCommit commit = walk.next();
if (commit == null) {
break;
}
links.addAll(JGitUtils.identifyTicketsFromCommitMessage(repository, settings, commit));
}
} catch (IOException e) {
LOGGER.error("failed to identify tickets between commits.", e);
} finally {
walk.dispose();
}
return links;
}
public static int countCommits(Repository repository, RevWalk walk, ObjectId baseId, ObjectId tipId) {
int count = 0;
walk.reset();
walk.sort(RevSort.TOPO);
walk.sort(RevSort.REVERSE, true);
try {
RevCommit tip = walk.parseCommit(tipId);
RevCommit base = walk.parseCommit(baseId);
walk.markStart(tip);
walk.markUninteresting(base);
for (;;) {
RevCommit c = walk.next();
if (c == null) {
break;
}
count++;
}
} catch (IOException e) {
// Should never happen, the core receive process would have
// identified the missing object earlier before we got control.
LOGGER.error("failed to get commit count", e);
return 0;
} finally {
walk.close();
}
return count;
}
public static int countCommits(Repository repository, RevWalk walk, String baseId, String tipId) {
int count = 0;
try {
count = countCommits(repository, walk, repository.resolve(baseId),repository.resolve(tipId));
} catch (IOException e) {
LOGGER.error("failed to get commit count", e);
}
return count;
}
}