Merge with 2.0.0-m3

This commit is contained in:
Rene Pfeuffer
2019-11-18 10:34:12 +01:00
691 changed files with 10293 additions and 153425 deletions

View File

@@ -4,5 +4,5 @@ import org.eclipse.jgit.lib.Repository;
import sonia.scm.repository.spi.GitContext;
import sonia.scm.repository.util.WorkdirFactory;
public interface GitWorkdirFactory extends WorkdirFactory<Repository, GitContext> {
public interface GitWorkdirFactory extends WorkdirFactory<Repository, Repository, GitContext> {
}

View File

@@ -142,7 +142,7 @@ class AbstractGitCommand
}
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) {
try (WorkingCopy<Repository> workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) {
try (WorkingCopy<Repository, Repository> workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) {
Repository repository = workingCopy.getWorkingRepository();
logger.debug("cloned repository to folder {}", repository.getWorkTree());
return workerSupplier.apply(new Git(repository)).run();
@@ -152,19 +152,28 @@ class AbstractGitCommand
}
ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException {
sonia.scm.repository.Repository scmRepository = context.getRepository();
return resolveRevisionOrThrowNotFound(repository, revision, scmRepository);
}
static ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision, sonia.scm.repository.Repository scmRepository) throws IOException {
ObjectId resolved = repository.resolve(revision);
if (resolved == null) {
throw notFound(entity("Revision", revision).in(context.getRepository()));
throw notFound(entity("Revision", revision).in(scmRepository));
} else {
return resolved;
}
}
abstract class GitCloneWorker<R> {
abstract static class GitCloneWorker<R> {
private final Git clone;
private final GitContext context;
private final sonia.scm.repository.Repository repository;
GitCloneWorker(Git clone) {
GitCloneWorker(Git clone, GitContext context, sonia.scm.repository.Repository repository) {
this.clone = clone;
this.context = context;
this.repository = repository;
}
abstract R run() throws IOException;
@@ -173,6 +182,10 @@ class AbstractGitCommand
return clone;
}
GitContext getContext() {
return context;
}
void checkOutBranch(String branchName) throws IOException {
try {
clone.checkout().setName(branchName).call();
@@ -199,7 +212,7 @@ class AbstractGitCommand
ObjectId resolveRevision(String revision) throws IOException {
ObjectId resolved = clone.getRepository().resolve(revision);
if (resolved == null) {
return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision);
return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision, context.getRepository());
} else {
return resolved;
}

View File

@@ -33,51 +33,120 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CannotDeleteCurrentBranchException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.util.WorkingCopy;
import sonia.scm.repository.api.HookBranchProvider;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.HookFeature;
import java.util.stream.StreamSupport;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class GitBranchCommand extends AbstractGitCommand implements BranchCommand {
private final GitWorkdirFactory workdirFactory;
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
GitBranchCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) {
GitBranchCommand(GitContext context, Repository repository, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
super(context, repository);
this.workdirFactory = workdirFactory;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
}
@Override
public Branch branch(BranchRequest request) {
try (WorkingCopy<org.eclipse.jgit.lib.Repository> workingCopy = workdirFactory.createWorkingCopy(context, request.getParentBranch())) {
Git clone = new Git(workingCopy.getWorkingRepository());
Ref ref = clone.branchCreate().setName(request.getNewBranch()).call();
Iterable<PushResult> call = clone.push().add(request.getNewBranch()).call();
StreamSupport.stream(call.spliterator(), false)
.flatMap(pushResult -> pushResult.getRemoteUpdates().stream())
.filter(remoteRefUpdate -> remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK)
.findFirst()
.ifPresent(r -> this.handlePushError(r, request, context.getRepository()));
try (Git git = new Git(context.open())) {
RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.createHookEvent(request.getNewBranch()));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
Ref ref = git.branchCreate().setStartPoint(request.getParentBranch()).setName(request.getNewBranch()).call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId()));
} catch (GitAPIException ex) {
} catch (GitAPIException | IOException ex) {
throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex);
}
}
private void handlePushError(RemoteRefUpdate remoteRefUpdate, BranchRequest request, Repository repository) {
if (remoteRefUpdate.getStatus() != RemoteRefUpdate.Status.OK) {
// TODO handle failed remote update
throw new IntegrateChangesFromWorkdirException(repository,
String.format("Could not push new branch '%s' into central repository", request.getNewBranch()));
@Override
public void deleteOrClose(String branchName) {
try (Git gitRepo = new Git(context.open())) {
RepositoryHookEvent hookEvent = createBranchHookEvent(BranchHookContextProvider.deleteHookEvent(branchName));
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
gitRepo
.branchDelete()
.setBranchNames(branchName)
.setForce(true)
.call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
} catch (CannotDeleteCurrentBranchException e) {
throw new CannotDeleteDefaultBranchException(context.getRepository(), branchName);
} catch (GitAPIException | IOException ex) {
throw new InternalRepositoryException(entity(context.getRepository()), String.format("Could not delete branch: %s", branchName));
}
}
private RepositoryHookEvent createBranchHookEvent(BranchHookContextProvider hookEvent) {
HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository());
return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE);
}
private static class BranchHookContextProvider extends HookContextProvider {
private final List<String> newBranches;
private final List<String> deletedBranches;
private BranchHookContextProvider(List<String> newBranches, List<String> deletedBranches) {
this.newBranches = newBranches;
this.deletedBranches = deletedBranches;
}
static BranchHookContextProvider createHookEvent(String newBranch) {
return new BranchHookContextProvider(singletonList(newBranch), emptyList());
}
static BranchHookContextProvider deleteHookEvent(String deletedBranch) {
return new BranchHookContextProvider(emptyList(), singletonList(deletedBranch));
}
@Override
public Set<HookFeature> getSupportedFeatures() {
return singleton(HookFeature.BRANCH_PROVIDER);
}
@Override
public HookBranchProvider getBranchProvider() {
return new HookBranchProvider() {
@Override
public List<String> getCreatedOrModified() {
return newBranches;
}
@Override
public List<String> getDeletedOrClosed() {
return deletedBranches;
}
};
}
@Override
public HookChangesetProvider getChangesetProvider() {
return r -> new HookChangesetResponse(emptyList());
}
}
}

View File

@@ -213,8 +213,14 @@ public class GitBrowseCommand extends AbstractGitCommand
if (lfsPointer.isPresent()) {
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
Blob blob = lfsBlobStore.get(lfsPointer.get().getOid().getName());
file.setLength(blob.getSize());
String oid = lfsPointer.get().getOid().getName();
Blob blob = lfsBlobStore.get(oid);
if (blob == null) {
logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName());
file.setLength(-1);
} else {
file.setLength(blob.getSize());
}
} else {
file.setLength(loader.getSize());
}

View File

@@ -145,7 +145,12 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException {
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
Blob blob = lfsBlobStore.get(lfsPointer.getOid().getName());
String oid = lfsPointer.getOid().getName();
Blob blob = lfsBlobStore.get(oid);
if (blob == null) {
logger.error("lfs blob for lob id {} not found in lfs store of repository {}", oid, repository.getNamespaceAndName());
throw notFound(entity("LFS", oid).in(repository));
}
GitUtil.release(revWalk);
GitUtil.release(treeWalk);
return new BlobLoader(blob);

View File

@@ -0,0 +1,36 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.MergeCommandResult;
import java.io.IOException;
class GitFastForwardIfPossible extends GitMergeStrategy {
private GitMergeStrategy fallbackMerge;
GitFastForwardIfPossible(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
super(clone, request, context, repository);
fallbackMerge = new GitMergeCommit(clone, request, context, repository);
}
@Override
MergeCommandResult run() throws IOException {
MergeResult fastForwardResult = mergeWithFastForwardOnlyMode();
if (fastForwardResult.getMergeStatus().isSuccessful()) {
push();
return MergeCommandResult.success();
} else {
return fallbackMerge.run();
}
}
private MergeResult mergeWithFastForwardOnlyMode() throws IOException {
MergeCommand mergeCommand = getClone().merge();
mergeCommand.setFastForward(MergeCommand.FastForwardMode.FF_ONLY);
return doMergeInClone(mergeCommand);
}
}

View File

@@ -106,7 +106,7 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
* @return
*/
@Override
public Changeset getChangeset(String revision)
public Changeset getChangeset(String revision, LogCommandRequest request)
{
if (logger.isDebugEnabled())
{
@@ -131,7 +131,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
if (commit != null)
{
converter = new GitChangesetConverter(gr, revWalk);
changeset = converter.createChangeset(commit);
if (isBranchRequested(request)) {
String branch = request.getBranch();
if (isMergedIntoBranch(gr, revWalk, commit, branch)) {
logger.trace("returning commit {} with branch {}", commit.getId(), branch);
changeset = converter.createChangeset(commit, branch);
} else {
logger.debug("returning null, because commit {} was not merged into branch {}", commit.getId(), branch);
}
} else {
changeset = converter.createChangeset(commit);
}
}
else if (logger.isWarnEnabled())
{
@@ -157,6 +168,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
return changeset;
}
private boolean isMergedIntoBranch(Repository repository, RevWalk revWalk, RevCommit commit, String branchName) throws IOException {
return revWalk.isMergedInto(commit, findHeadCommitOfBranch(repository, revWalk, branchName));
}
private boolean isBranchRequested(LogCommandRequest request) {
return request != null && !Strings.isNullOrEmpty(request.getBranch());
}
private RevCommit findHeadCommitOfBranch(Repository repository, RevWalk revWalk, String branchName) throws IOException {
return revWalk.parseCommit(GitUtil.getCommit(repository, revWalk, repository.findRef(branchName)));
}
/**
* Method description
*

View File

@@ -1,36 +1,30 @@
package sonia.scm.repository.spi;
import com.google.common.base.Strings;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import com.google.common.collect.ImmutableSet;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.ResolveMerger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.MergeStrategy;
import sonia.scm.repository.api.MergeStrategyNotSupportedException;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Set;
import static org.eclipse.jgit.merge.MergeStrategy.RECURSIVE;
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n",
"Merge of branch {0} into {1}",
"",
"Automatic merge by SCM-Manager.");
private final GitWorkdirFactory workdirFactory;
private static final Set<MergeStrategy> STRATEGIES = ImmutableSet.of(
MergeStrategy.MERGE_COMMIT,
MergeStrategy.FAST_FORWARD_IF_POSSIBLE,
MergeStrategy.SQUASH
);
GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) {
super(context, repository);
this.workdirFactory = workdirFactory;
@@ -38,14 +32,30 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
@Override
public MergeCommandResult merge(MergeCommandRequest request) {
return inClone(clone -> new MergeWorker(clone, request), workdirFactory, request.getTargetBranch());
return mergeWithStrategy(request);
}
private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) {
switch(request.getMergeStrategy()) {
case SQUASH:
return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workdirFactory, request.getTargetBranch());
case FAST_FORWARD_IF_POSSIBLE:
return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workdirFactory, request.getTargetBranch());
case MERGE_COMMIT:
return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workdirFactory, request.getTargetBranch());
default:
throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy());
}
}
@Override
public MergeDryRunCommandResult dryRun(MergeCommandRequest request) {
try {
Repository repository = context.open();
ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
ResolveMerger merger = (ResolveMerger) RECURSIVE.newMerger(repository, true);
return new MergeDryRunCommandResult(
merger.merge(
resolveRevisionOrThrowNotFound(repository, request.getBranchToMerge()),
@@ -55,64 +65,14 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
}
}
private class MergeWorker extends GitCloneWorker<MergeCommandResult> {
private final String target;
private final String toMerge;
private final Person author;
private final String messageTemplate;
private MergeWorker(Git clone, MergeCommandRequest request) {
super(clone);
this.target = request.getTargetBranch();
this.toMerge = request.getBranchToMerge();
this.author = request.getAuthor();
this.messageTemplate = request.getMessageTemplate();
}
@Override
MergeCommandResult run() throws IOException {
MergeResult result = doMergeInClone();
if (result.getMergeStatus().isSuccessful()) {
doCommit();
push();
return MergeCommandResult.success();
} else {
return analyseFailure(result);
}
}
private MergeResult doMergeInClone() throws IOException {
MergeResult result;
try {
ObjectId sourceRevision = resolveRevision(toMerge);
result = getClone().merge()
.setFastForward(FastForwardMode.NO_FF)
.setCommit(false) // we want to set the author manually
.include(toMerge, sourceRevision)
.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + toMerge + " into " + target, e);
}
return result;
}
private void doCommit() {
logger.debug("merged branch {} into {}", toMerge, target);
doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author);
}
private String determineMessageTemplate() {
if (Strings.isNullOrEmpty(messageTemplate)) {
return MERGE_COMMIT_MESSAGE_TEMPLATE;
} else {
return messageTemplate;
}
}
private MergeCommandResult analyseFailure(MergeResult result) {
logger.info("could not merged branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
return MergeCommandResult.failure(result.getConflicts().keySet());
}
@Override
public boolean isSupported(MergeStrategy strategy) {
return STRATEGIES.contains(strategy);
}
@Override
public Set<MergeStrategy> getSupportedMergeStrategies() {
return STRATEGIES;
}
}

View File

@@ -0,0 +1,31 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.MergeCommandResult;
import java.io.IOException;
class GitMergeCommit extends GitMergeStrategy {
GitMergeCommit(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
super(clone, request, context, repository);
}
@Override
MergeCommandResult run() throws IOException {
MergeCommand mergeCommand = getClone().merge();
mergeCommand.setFastForward(MergeCommand.FastForwardMode.NO_FF);
MergeResult result = doMergeInClone(mergeCommand);
if (result.getMergeStatus().isSuccessful()) {
doCommit();
push();
return MergeCommandResult.success();
} else {
return analyseFailure(result);
}
}
}

View File

@@ -0,0 +1,72 @@
package sonia.scm.repository.spi;
import com.google.common.base.Strings;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import java.io.IOException;
import java.text.MessageFormat;
abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker<MergeCommandResult> {
private static final Logger logger = LoggerFactory.getLogger(GitMergeStrategy.class);
private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n",
"Merge of branch {0} into {1}",
"",
"Automatic merge by SCM-Manager.");
private final String target;
private final String toMerge;
private final Person author;
private final String messageTemplate;
GitMergeStrategy(Git clone, MergeCommandRequest request, GitContext context, sonia.scm.repository.Repository repository) {
super(clone, context, repository);
this.target = request.getTargetBranch();
this.toMerge = request.getBranchToMerge();
this.author = request.getAuthor();
this.messageTemplate = request.getMessageTemplate();
}
MergeResult doMergeInClone(MergeCommand mergeCommand) throws IOException {
MergeResult result;
try {
ObjectId sourceRevision = resolveRevision(toMerge);
mergeCommand
.setCommit(false) // we want to set the author manually
.include(toMerge, sourceRevision);
result = mergeCommand.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(getContext().getRepository(), "could not merge branch " + toMerge + " into " + target, e);
}
return result;
}
void doCommit() {
logger.debug("merged branch {} into {}", toMerge, target);
doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author);
}
private String determineMessageTemplate() {
if (Strings.isNullOrEmpty(messageTemplate)) {
return MERGE_COMMIT_MESSAGE_TEMPLATE;
} else {
return messageTemplate;
}
}
MergeCommandResult analyseFailure(MergeResult result) {
logger.info("could not merge branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
return MergeCommandResult.failure(result.getConflicts().keySet());
}
}

View File

@@ -0,0 +1,31 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.MergeCommandResult;
import org.eclipse.jgit.api.MergeCommand;
import java.io.IOException;
class GitMergeWithSquash extends GitMergeStrategy {
GitMergeWithSquash(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
super(clone, request, context, repository);
}
@Override
MergeCommandResult run() throws IOException {
MergeCommand mergeCommand = getClone().merge();
mergeCommand.setSquash(true);
MergeResult result = doMergeInClone(mergeCommand);
if (result.getMergeStatus().isSuccessful()) {
doCommit();
push();
return MergeCommandResult.success();
} else {
return analyseFailure(result);
}
}
}

View File

@@ -46,7 +46,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
private final ModifyCommandRequest request;
ModifyWorker(Git clone, ModifyCommandRequest request) {
super(clone);
super(clone, context, repository);
this.workDir = clone.getRepository().getWorkTree();
this.request = request;
}

View File

@@ -35,10 +35,12 @@ package sonia.scm.repository.spi;
import com.google.common.collect.ImmutableSet;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Feature;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.IOException;
@@ -64,6 +66,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
Command.DIFF_RESULT,
Command.LOG,
Command.TAGS,
Command.BRANCH,
Command.BRANCHES,
Command.INCOMING,
Command.OUTGOING,
@@ -77,10 +80,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
//~--- constructors ---------------------------------------------------------
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) {
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
this.handler = handler;
this.repository = repository;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
}
@@ -133,7 +138,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
@Override
public BranchCommand getBranchCommand()
{
return new GitBranchCommand(context, repository, handler.getWorkdirFactory());
return new GitBranchCommand(context, repository, hookContextFactory, eventBus);
}
/**
@@ -292,4 +297,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
private final Repository repository;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
}

View File

@@ -36,9 +36,11 @@ package sonia.scm.repository.spi;
import com.google.inject.Inject;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.event.ScmEventBus;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
/**
@@ -51,12 +53,16 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
private final GitRepositoryHandler handler;
private final GitRepositoryConfigStoreProvider storeProvider;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final HookContextFactory hookContextFactory;
private final ScmEventBus eventBus;
@Inject
public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) {
public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
this.handler = handler;
this.storeProvider = storeProvider;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.hookContextFactory = hookContextFactory;
this.eventBus = eventBus;
}
@Override
@@ -64,7 +70,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
GitRepositoryServiceProvider provider = null;
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory);
provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory, hookContextFactory, eventBus);
}
return provider;

View File

@@ -18,7 +18,7 @@ import java.io.IOException;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, Repository, GitContext> implements GitWorkdirFactory {
@Inject
public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) {
@@ -26,7 +26,7 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, Gi
}
@Override
public ParentAndClone<Repository> cloneRepository(GitContext context, File target, String initialBranch) {
public ParentAndClone<Repository, Repository> cloneRepository(GitContext context, File target, String initialBranch) {
try {
Repository clone = Git.cloneRepository()
.setURI(createScmTransportProtocolUri(context.getDirectory()))
@@ -60,6 +60,13 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, Gi
}
}
@Override
protected void closeWorkdirInternal(Repository workdir) throws Exception {
if (workdir != null) {
workdir.close();
}
}
@Override
protected sonia.scm.repository.Repository getScmRepository(GitContext context) {
return context.getRepository();

View File

@@ -0,0 +1,28 @@
package sonia.scm.web.lfs;
import org.eclipse.jgit.lfs.server.Response;
import sonia.scm.security.AccessToken;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.TimeZone;
class ExpiringAction extends Response.Action {
@SuppressWarnings({"squid:S00116"})
// This class is used for json serialization, only
public final String expires_at;
ExpiringAction(String href, AccessToken accessToken) {
this.expires_at = createDateFormat().format(accessToken.getExpiration());
this.href = href;
this.header = Collections.singletonMap("Authorization", "Bearer " + accessToken.compact());
}
private DateFormat createDateFormat() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
return dateFormat;
}
}

View File

@@ -0,0 +1,98 @@
package sonia.scm.web.lfs;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension;
import sonia.scm.protocolcommand.CommandInterpreter;
import sonia.scm.protocolcommand.CommandInterpreterFactory;
import sonia.scm.protocolcommand.RepositoryContext;
import sonia.scm.protocolcommand.RepositoryContextResolver;
import sonia.scm.protocolcommand.ScmCommandProtocol;
import sonia.scm.protocolcommand.git.GitRepositoryContextResolver;
import sonia.scm.repository.Repository;
import sonia.scm.security.AccessToken;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Optional;
import static java.lang.String.format;
@Extension
public class LFSAuthCommand implements CommandInterpreterFactory {
private static final Logger LOG = LoggerFactory.getLogger(LFSAuthCommand.class);
private static final String LFS_INFO_URL_PATTERN = "%s/repo/%s/%s.git/info/lfs/";
private final LfsAccessTokenFactory tokenFactory;
private final GitRepositoryContextResolver gitRepositoryContextResolver;
private final ObjectMapper objectMapper;
private final ScmConfiguration configuration;
@Inject
public LFSAuthCommand(LfsAccessTokenFactory tokenFactory, GitRepositoryContextResolver gitRepositoryContextResolver, ScmConfiguration configuration) {
this.tokenFactory = tokenFactory;
this.gitRepositoryContextResolver = gitRepositoryContextResolver;
objectMapper = new ObjectMapper();
this.configuration = configuration;
}
@Override
public Optional<CommandInterpreter> canHandle(String command) {
if (command.startsWith("git-lfs-authenticate")) {
LOG.trace("create command for input: {}", command);
return Optional.of(new LfsAuthCommandInterpreter(command));
} else {
return Optional.empty();
}
}
private class LfsAuthCommandInterpreter implements CommandInterpreter {
private final String command;
LfsAuthCommandInterpreter(String command) {
this.command = command;
}
@Override
public String[] getParsedArgs() {
// we are interested only in the 'repo' argument, so we discard the rest
return new String[]{command.split("\\s+")[1]};
}
@Override
public ScmCommandProtocol getProtocolHandler() {
return (context, repositoryContext) -> {
ExpiringAction response = createResponseObject(repositoryContext);
// we buffer the response and write it with a single write,
// because otherwise the ssh connection is not closed
String buffer = serializeResponse(response);
context.getOutputStream().write(buffer.getBytes(Charsets.UTF_8));
};
}
@Override
public RepositoryContextResolver getRepositoryContextResolver() {
return gitRepositoryContextResolver;
}
private ExpiringAction createResponseObject(RepositoryContext repositoryContext) {
Repository repository = repositoryContext.getRepository();
String url = format(LFS_INFO_URL_PATTERN, configuration.getBaseUrl(), repository.getNamespace(), repository.getName());
AccessToken accessToken = tokenFactory.createReadAccessToken(repository);
return new ExpiringAction(url, accessToken);
}
private String serializeResponse(ExpiringAction response) throws IOException {
return objectMapper.writeValueAsString(response);
}
}
}

View File

@@ -0,0 +1,70 @@
package sonia.scm.web.lfs;
import com.github.sdorra.ssp.PermissionCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.security.AccessToken;
import sonia.scm.security.AccessTokenBuilderFactory;
import sonia.scm.security.Scope;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class LfsAccessTokenFactory {
private static final Logger LOG = LoggerFactory.getLogger(LfsAccessTokenFactory.class);
private final AccessTokenBuilderFactory tokenBuilderFactory;
@Inject
LfsAccessTokenFactory(AccessTokenBuilderFactory tokenBuilderFactory) {
this.tokenBuilderFactory = tokenBuilderFactory;
}
AccessToken createReadAccessToken(Repository repository) {
PermissionCheck read = RepositoryPermissions.read(repository);
read.check();
PermissionCheck pull = RepositoryPermissions.pull(repository);
pull.check();
List<String> permissions = new ArrayList<>();
permissions.add(read.asShiroString());
permissions.add(pull.asShiroString());
PermissionCheck push = RepositoryPermissions.push(repository);
if (push.isPermitted()) {
// we have to add push permissions,
// because this token is also used to obtain the write access token
permissions.add(push.asShiroString());
}
return createToken(Scope.valueOf(permissions));
}
AccessToken createWriteAccessToken(Repository repository) {
PermissionCheck read = RepositoryPermissions.read(repository);
read.check();
PermissionCheck pull = RepositoryPermissions.pull(repository);
pull.check();
PermissionCheck push = RepositoryPermissions.push(repository);
push.check();
return createToken(Scope.valueOf(read.asShiroString(), pull.asShiroString(), push.asShiroString()));
}
private AccessToken createToken(Scope scope) {
LOG.trace("create access token with scope: {}", scope);
return tokenBuilderFactory
.create()
.expiresIn(5, TimeUnit.MINUTES)
.scope(scope)
.build();
}
}

View File

@@ -2,12 +2,13 @@ package sonia.scm.web.lfs;
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
import org.eclipse.jgit.lfs.server.LargeFileRepository;
import org.eclipse.jgit.lfs.server.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Repository;
import sonia.scm.security.AccessToken;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import java.io.IOException;
/**
* This LargeFileRepository is used for jGit-Servlet implementation. Under the jgit LFS Servlet hood, the
* SCM-Repository API is used to implement the Repository.
@@ -17,49 +18,67 @@ import java.io.IOException;
*/
public class ScmBlobLfsRepository implements LargeFileRepository {
private static final Logger LOG = LoggerFactory.getLogger(ScmBlobLfsRepository.class);
private final BlobStore blobStore;
private final LfsAccessTokenFactory tokenFactory;
/**
* This URI is used to determine the actual URI for Upload / Download. Must be full URI (or rewritable by reverse
* proxy).
*/
private final String baseUri;
private final Repository repository;
/**
* A {@link ScmBlobLfsRepository} is created for either download or upload, not both. Therefore we can cache the
* access token and do not have to create them anew for each action.
*/
private AccessToken accessToken;
/**
* Creates a {@link ScmBlobLfsRepository} for the provided repository.
*
* @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}.
* @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or
* rewritable by reverse proxy).
* @param repository The current scm repository this LFS repository is used for.
* @param blobStore The SCM Blobstore used for this @{@link LargeFileRepository}.
* @param tokenFactory The token builder for subsequent LFS requests.
* @param baseUri This URI is used to determine the actual URI for Upload / Download. Must be full URI (or
*/
public ScmBlobLfsRepository(BlobStore blobStore, String baseUri) {
public ScmBlobLfsRepository(Repository repository, BlobStore blobStore, LfsAccessTokenFactory tokenFactory, String baseUri) {
this.repository = repository;
this.blobStore = blobStore;
this.tokenFactory = tokenFactory;
this.baseUri = baseUri;
}
@Override
public Response.Action getDownloadAction(AnyLongObjectId id) {
return getAction(id);
public ExpiringAction getDownloadAction(AnyLongObjectId id) {
if (accessToken == null) {
LOG.trace("create access token to download lfs object {} from repository {}", id, repository.getNamespaceAndName());
accessToken = tokenFactory.createReadAccessToken(repository);
}
return getAction(id, accessToken);
}
@Override
public Response.Action getUploadAction(AnyLongObjectId id, long size) {
return getAction(id);
public ExpiringAction getUploadAction(AnyLongObjectId id, long size) {
if (accessToken == null) {
LOG.trace("create access token to upload lfs object {} to repository {}", id, repository.getNamespaceAndName());
accessToken = tokenFactory.createWriteAccessToken(repository);
}
return getAction(id, accessToken);
}
@Override
public Response.Action getVerifyAction(AnyLongObjectId id) {
public ExpiringAction getVerifyAction(AnyLongObjectId id) {
//validation is optional. We do not support it.
return null;
}
@Override
public long getSize(AnyLongObjectId id) throws IOException {
public long getSize(AnyLongObjectId id) {
//this needs to be size of what is will be written into the response of the download. Clients are likely to
// verify it.
@@ -77,14 +96,11 @@ public class ScmBlobLfsRepository implements LargeFileRepository {
/**
* Constructs the Download / Upload actions to be supplied to the client.
*/
private Response.Action getAction(AnyLongObjectId id) {
private ExpiringAction getAction(AnyLongObjectId id, AccessToken token) {
//LFS protocol has to provide the information on where to put or get the actual content, i. e.
//the actual URI for up- and download.
Response.Action a = new Response.Action();
a.href = baseUri + id.getName();
return a;
return new ExpiringAction(baseUri + id.getName(), token);
}
}

View File

@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.repository.Repository;
import sonia.scm.store.BlobStore;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.lfs.LfsAccessTokenFactory;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import sonia.scm.web.lfs.ScmBlobLfsRepository;
@@ -27,13 +28,15 @@ import javax.servlet.http.HttpServletRequest;
@Singleton
public class LfsServletFactory {
private static final Logger logger = LoggerFactory.getLogger(LfsServletFactory.class);
private static final Logger LOG = LoggerFactory.getLogger(LfsServletFactory.class);
private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final LfsAccessTokenFactory tokenFactory;
@Inject
public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory) {
public LfsServletFactory(LfsBlobStoreFactory lfsBlobStoreFactory, LfsAccessTokenFactory tokenFactory) {
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.tokenFactory = tokenFactory;
}
/**
@@ -44,10 +47,11 @@ public class LfsServletFactory {
* @return The {@link LfsProtocolServlet} to provide the LFS Batch API for a SCM Repository.
*/
public LfsProtocolServlet createProtocolServletFor(Repository repository, HttpServletRequest request) {
LOG.trace("create lfs protocol servlet for repository {}", repository.getNamespaceAndName());
BlobStore blobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
String baseUri = buildBaseUri(repository, request);
LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(blobStore, baseUri);
LargeFileRepository largeFileRepository = new ScmBlobLfsRepository(repository, blobStore, tokenFactory, baseUri);
return new ScmLfsProtocolServlet(largeFileRepository);
}
@@ -59,6 +63,7 @@ public class LfsServletFactory {
* @return The {@link FileLfsServlet} to provide the LFS Upload / Download API for a SCM Repository.
*/
public HttpServlet createFileLfsServletFor(Repository repository, HttpServletRequest request) {
LOG.trace("create lfs file servlet for repository {}", repository.getNamespaceAndName());
return new ScmFileTransferServlet(lfsBlobStoreFactory.getLfsBlobStore(repository));
}