/* | |
* 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; | |
} | |
} |