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));
}

View File

@@ -1,14 +1,10 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { Repository } from "@scm-manager/ui-types";
import { WithTranslation, withTranslation } from "react-i18next";
import { Repository } from "@scm-manager/ui-types";
type Props = {
url: string,
repository: Repository,
// context props
t: string => string
type Props = WithTranslation & {
url: string;
repository: Repository;
};
class CloneInformation extends React.Component<Props> {
@@ -28,7 +24,8 @@ class CloneInformation extends React.Component<Props> {
<br />
cd {repository.name}
<br />
echo "# {repository.name}" > README.md
echo "# {repository.name}
" > README.md
<br />
git add README.md
<br />
@@ -54,4 +51,4 @@ class CloneInformation extends React.Component<Props> {
}
}
export default translate("plugins")(CloneInformation);
export default withTranslation("plugins")(CloneInformation);

View File

@@ -1,16 +1,12 @@
//@flow
import React from "react";
import { Image } from "@scm-manager/ui-components";
type Props = {
};
type Props = {};
class GitAvatar extends React.Component<Props> {
render() {
return <Image src="/images/git-logo.png" alt="Git Logo" />;
}
}
export default GitAvatar;

View File

@@ -1,11 +1,9 @@
//@flow
import React from "react";
import type { Branch } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import { WithTranslation, withTranslation } from "react-i18next";
import { Branch } from "@scm-manager/ui-types";
type Props = {
branch: Branch,
t: string => string
type Props = WithTranslation & {
branch: Branch;
};
class GitBranchInformation extends React.Component<Props> {
@@ -27,4 +25,4 @@ class GitBranchInformation extends React.Component<Props> {
}
}
export default translate("plugins")(GitBranchInformation);
export default withTranslation("plugins")(GitBranchInformation);

View File

@@ -1,25 +1,20 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { Links } from "@scm-manager/ui-types";
import { WithTranslation, withTranslation } from "react-i18next";
import { Links } from "@scm-manager/ui-types";
import { InputField, Checkbox } from "@scm-manager/ui-components";
type Configuration = {
repositoryDirectory?: string,
gcExpression?: string,
nonFastForwardDisallowed: boolean,
_links: Links
repositoryDirectory?: string;
gcExpression?: string;
nonFastForwardDisallowed: boolean;
_links: Links;
};
type Props = {
initialConfiguration: Configuration,
readOnly: boolean,
type Props = WithTranslation & {
initialConfiguration: Configuration;
readOnly: boolean;
onConfigurationChange: (Configuration, boolean) => void,
// context props
t: string => string
onConfigurationChange: (p1: Configuration, p2: boolean) => void;
};
type State = Configuration & {};
@@ -27,13 +22,24 @@ type State = Configuration & {};
class GitConfigurationForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { ...props.initialConfiguration };
this.state = {
...props.initialConfiguration
};
}
handleChange = (value: any, name: string) => {
onGcExpressionChange = (value: string) => {
this.setState(
{
[name]: value
gcExpression: value
},
() => this.props.onConfigurationChange(this.state, true)
);
};
onNonFastForwardDisallowed = (value: boolean) => {
this.setState(
{
nonFastForwardDisallowed: value
},
() => this.props.onConfigurationChange(this.state, true)
);
@@ -50,7 +56,7 @@ class GitConfigurationForm extends React.Component<Props, State> {
label={t("scm-git-plugin.config.gcExpression")}
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}
value={gcExpression}
onChange={this.handleChange}
onChange={this.onGcExpressionChange}
disabled={readOnly}
/>
<Checkbox
@@ -58,7 +64,7 @@ class GitConfigurationForm extends React.Component<Props, State> {
label={t("scm-git-plugin.config.nonFastForwardDisallowed")}
helpText={t("scm-git-plugin.config.nonFastForwardDisallowedHelpText")}
checked={nonFastForwardDisallowed}
onChange={this.handleChange}
onChange={this.onNonFastForwardDisallowed}
disabled={readOnly}
/>
</>
@@ -66,4 +72,4 @@ class GitConfigurationForm extends React.Component<Props, State> {
}
}
export default translate("plugins")(GitConfigurationForm);
export default withTranslation("plugins")(GitConfigurationForm);

View File

@@ -1,32 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { Title, Configuration } from "@scm-manager/ui-components";
import GitConfigurationForm from "./GitConfigurationForm";
type Props = {
link: string,
t: (string) => string
};
class GitGlobalConfiguration extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
render() {
const { link, t } = this.props;
return (
<div>
<Title title={t("scm-git-plugin.config.title")}/>
<Configuration link={link} render={props => <GitConfigurationForm {...props} />}/>
</div>
);
}
}
export default translate("plugins")(GitGlobalConfiguration);

View File

@@ -0,0 +1,23 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Title, Configuration } from "@scm-manager/ui-components";
import GitConfigurationForm from "./GitConfigurationForm";
type Props = WithTranslation & {
link: string;
};
class GitGlobalConfiguration extends React.Component<Props> {
render() {
const { link, t } = this.props;
return (
<div>
<Title title={t("scm-git-plugin.config.title")} />
<Configuration link={link} render={(props: any) => <GitConfigurationForm {...props} />} />
</div>
);
}
}
export default withTranslation("plugins")(GitGlobalConfiguration);

View File

@@ -1,13 +1,11 @@
//@flow
import React from "react";
import type { Repository } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import { WithTranslation, withTranslation } from "react-i18next";
import { Repository } from "@scm-manager/ui-types";
type Props = {
repository: Repository,
target: string,
source: string,
t: string => string
type Props = WithTranslation & {
repository: Repository;
target: string;
source: string;
};
class GitMergeInformation extends React.Component<Props> {
@@ -23,21 +21,15 @@ class GitMergeInformation extends React.Component<Props> {
</pre>
{t("scm-git-plugin.information.merge.update")}
<pre>
<code>
git pull
</code>
<code>git pull</code>
</pre>
{t("scm-git-plugin.information.merge.merge")}
<pre>
<code>
git merge {source}
</code>
<code>git merge {source}</code>
</pre>
{t("scm-git-plugin.information.merge.resolve")}
<pre>
<code>
git add &lt;conflict file&gt;
</code>
<code>git add &lt;conflict file&gt;</code>
</pre>
{t("scm-git-plugin.information.merge.commit")}
<pre>
@@ -47,13 +39,11 @@ class GitMergeInformation extends React.Component<Props> {
</pre>
{t("scm-git-plugin.information.merge.push")}
<pre>
<code>
git push
</code>
<code>git push</code>
</pre>
</div>
);
}
}
export default translate("plugins")(GitMergeInformation);
export default withTranslation("plugins")(GitMergeInformation);

View File

@@ -1,7 +1,6 @@
//@flow
import React from "react";
import styled from "styled-components";
import type { Repository, Link } from "@scm-manager/ui-types";
import { Repository, Link } from "@scm-manager/ui-types";
import { ButtonAddons, Button } from "@scm-manager/ui-components";
import CloneInformation from "./CloneInformation";
@@ -16,17 +15,17 @@ const Switcher = styled(ButtonAddons)`
`;
type Props = {
repository: Repository
repository: Repository;
};
type State = {
selected?: Link
selected?: Link;
};
function selectHttpOrFirst(repository: Repository) {
const protocols = repository._links["protocol"] || [];
const protocols = (repository._links["protocol"] as Link[]) || [];
for (let protocol of protocols) {
for (const protocol of protocols) {
if (protocol.name === "http") {
return protocol;
}
@@ -55,7 +54,7 @@ export default class ProtocolInformation extends React.Component<Props, State> {
renderProtocolButton = (protocol: Link) => {
const name = protocol.name || "unknown";
let color = null;
let color;
const { selected } = this.state;
if (selected && protocol.name === selected.name) {
@@ -72,23 +71,19 @@ export default class ProtocolInformation extends React.Component<Props, State> {
render() {
const { repository } = this.props;
const protocols = repository._links["protocol"];
const protocols = repository._links["protocol"] as Link[];
if (!protocols || protocols.length === 0) {
return null;
}
if (protocols.length === 1) {
return (
<CloneInformation url={protocols[0].href} repository={repository} />
);
return <CloneInformation url={protocols[0].href} repository={repository} />;
}
const { selected } = this.state;
let cloneInformation = null;
if (selected) {
cloneInformation = (
<CloneInformation repository={repository} url={selected.href} />
);
cloneInformation = <CloneInformation repository={repository} url={selected.href} />;
}
return (

View File

@@ -1,31 +1,26 @@
// @flow
import React, { FormEvent } from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Branch, Repository, Link } from "@scm-manager/ui-types";
import { apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton } from "@scm-manager/ui-components";
import React from "react";
import {apiClient, BranchSelector, ErrorPage, Loading, Subtitle, SubmitButton} from "@scm-manager/ui-components";
import type {Branch, Repository} from "@scm-manager/ui-types";
import {translate} from "react-i18next";
type Props = {
repository: Repository,
t: string => string
type Props = WithTranslation & {
repository: Repository;
};
type State = {
loadingBranches: boolean,
loadingDefaultBranch: boolean,
submitPending: boolean,
error?: Error,
branches: Branch[],
selectedBranchName?: string,
defaultBranchChanged: boolean,
disabled: boolean
loadingBranches: boolean;
loadingDefaultBranch: boolean;
submitPending: boolean;
error?: Error;
branches: Branch[];
selectedBranchName?: string;
defaultBranchChanged: boolean;
disabled: boolean;
};
const GIT_CONFIG_CONTENT_TYPE = "application/vnd.scmm-gitConfig+json";
class RepositoryConfig extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
@@ -41,19 +36,36 @@ class RepositoryConfig extends React.Component<Props, State> {
componentDidMount() {
const { repository } = this.props;
this.setState({ ...this.state, loadingBranches: true });
this.setState({
...this.state,
loadingBranches: true
});
const branchesLink = repository._links.branches as Link;
apiClient
.get(repository._links.branches.href)
.get(branchesLink.href)
.then(response => response.json())
.then(payload => payload._embedded.branches)
.then(branches =>
this.setState({ ...this.state, branches, loadingBranches: false })
this.setState({
...this.state,
branches,
loadingBranches: false
})
)
.catch(error => this.setState({ ...this.state, error }));
.catch(error =>
this.setState({
...this.state,
error
})
);
this.setState({ ...this.state, loadingDefaultBranch: true });
const configurationLink = repository._links.configuration as Link;
this.setState({
...this.state,
loadingDefaultBranch: true
});
apiClient
.get(repository._links.configuration.href)
.get(configurationLink.href)
.then(response => response.json())
.then(payload =>
this.setState({
@@ -63,31 +75,44 @@ class RepositoryConfig extends React.Component<Props, State> {
loadingDefaultBranch: false
})
)
.catch(error => this.setState({ ...this.state, error }));
.catch(error =>
this.setState({
...this.state,
error
})
);
}
branchSelected = (branch: Branch) => {
branchSelected = (branch?: Branch) => {
if (!branch) {
this.setState({ ...this.state, selectedBranchName: undefined, defaultBranchChanged: false});
this.setState({
...this.state,
selectedBranchName: undefined,
defaultBranchChanged: false
});
return;
}
this.setState({ ...this.state, selectedBranchName: branch.name, defaultBranchChanged: false });
this.setState({
...this.state,
selectedBranchName: branch.name,
defaultBranchChanged: false
});
};
submit = (event: Event) => {
submit = (event: FormEvent) => {
event.preventDefault();
const { repository } = this.props;
const newConfig = {
defaultBranch: this.state.selectedBranchName
};
this.setState({ ...this.state, submitPending: true });
this.setState({
...this.state,
submitPending: true
});
const configurationLink = repository._links.configuration as Link;
apiClient
.put(
repository._links.configuration.href,
newConfig,
GIT_CONFIG_CONTENT_TYPE
)
.put(configurationLink.href, newConfig, GIT_CONFIG_CONTENT_TYPE)
.then(() =>
this.setState({
...this.state,
@@ -95,7 +120,12 @@ class RepositoryConfig extends React.Component<Props, State> {
defaultBranchChanged: true
})
)
.catch(error => this.setState({ ...this.state, error }));
.catch(error =>
this.setState({
...this.state,
error
})
);
};
render() {
@@ -112,17 +142,19 @@ class RepositoryConfig extends React.Component<Props, State> {
);
}
const submitButton = disabled? null: <SubmitButton
label={t("scm-git-plugin.repo-config.submit")}
loading={submitPending}
disabled={!this.state.selectedBranchName}
/>;
const submitButton = disabled ? null : (
<SubmitButton
label={t("scm-git-plugin.repo-config.submit")}
loading={submitPending}
disabled={!this.state.selectedBranchName}
/>
);
if (!(loadingBranches || loadingDefaultBranch)) {
return (
<>
<hr />
<Subtitle subtitle={t("scm-git-plugin.repo-config.title")}/>
<Subtitle subtitle={t("scm-git-plugin.repo-config.title")} />
{this.renderBranchChangedNotification()}
<form onSubmit={this.submit}>
<BranchSelector
@@ -132,7 +164,7 @@ class RepositoryConfig extends React.Component<Props, State> {
selectedBranch={this.state.selectedBranchName}
disabled={disabled}
/>
{ submitButton }
{submitButton}
</form>
</>
);
@@ -148,7 +180,10 @@ class RepositoryConfig extends React.Component<Props, State> {
<button
className="delete"
onClick={() =>
this.setState({ ...this.state, defaultBranchChanged: false })
this.setState({
...this.state,
defaultBranchChanged: false
})
}
/>
{this.props.t("scm-git-plugin.repo-config.success")}
@@ -159,4 +194,4 @@ class RepositoryConfig extends React.Component<Props, State> {
};
}
export default translate("plugins")(RepositoryConfig);
export default withTranslation("plugins")(RepositoryConfig);

View File

@@ -1,16 +0,0 @@
// @flow
import "@scm-manager/ui-tests/i18n";
import { gitPredicate } from "./index";
describe("test git predicate", () => {
it("should return false", () => {
expect(gitPredicate()).toBe(false);
expect(gitPredicate({})).toBe(false);
expect(gitPredicate({ repository: {} })).toBe(false);
expect(gitPredicate({ repository: { type: "hg" } })).toBe(false);
});
it("should return true", () => {
expect(gitPredicate({ repository: { type: "git" } })).toBe(true);
});
});

View File

@@ -0,0 +1,31 @@
import "@scm-manager/ui-tests/i18n";
import { gitPredicate } from "./index";
describe("test git predicate", () => {
it("should return false", () => {
expect(gitPredicate(undefined)).toBe(false);
expect(gitPredicate({})).toBe(false);
expect(
gitPredicate({
repository: {}
})
).toBe(false);
expect(
gitPredicate({
repository: {
type: "hg"
}
})
).toBe(false);
});
it("should return true", () => {
expect(
gitPredicate({
repository: {
type: "git"
}
})
).toBe(true);
});
});

View File

@@ -1,10 +1,9 @@
//@flow
import React from "react";
import {binder} from "@scm-manager/ui-extensions";
import { binder } from "@scm-manager/ui-extensions";
import ProtocolInformation from "./ProtocolInformation";
import GitAvatar from "./GitAvatar";
import {ConfigurationBinder as cfgBinder} from "@scm-manager/ui-components";
import { ConfigurationBinder as cfgBinder } from "@scm-manager/ui-components";
import GitGlobalConfiguration from "./GitGlobalConfiguration";
import GitBranchInformation from "./GitBranchInformation";
import GitMergeInformation from "./GitMergeInformation";
@@ -13,33 +12,16 @@ import RepositoryConfig from "./RepositoryConfig";
// repository
// @visibleForTesting
export const gitPredicate = (props: Object) => {
export const gitPredicate = (props: any) => {
return !!(props && props.repository && props.repository.type === "git");
};
binder.bind(
"repos.repository-details.information",
ProtocolInformation,
gitPredicate
);
binder.bind(
"repos.branch-details.information",
GitBranchInformation,
gitPredicate
);
binder.bind(
"repos.repository-merge.information",
GitMergeInformation,
gitPredicate
);
binder.bind("repos.repository-details.information", ProtocolInformation, gitPredicate);
binder.bind("repos.branch-details.information", GitBranchInformation, gitPredicate);
binder.bind("repos.repository-merge.information", GitMergeInformation, gitPredicate);
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
binder.bind("repo-config.route", RepositoryConfig, gitPredicate);
// global config
cfgBinder.bindGlobal(
"/git",
"scm-git-plugin.config.link",
"gitConfig",
GitGlobalConfiguration
);
cfgBinder.bindGlobal("/git", "scm-git-plugin.config.link", "gitConfig", GitGlobalConfiguration);

View File

@@ -1,20 +1,37 @@
package sonia.scm.repository.spi;
import org.assertj.core.api.Assertions;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
import java.io.IOException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class GitBranchCommandTest extends AbstractGitCommandTestBase {
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Mock
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
@Test
public void shouldCreateBranchWithDefinedSourceBranch() throws IOException {
@@ -26,10 +43,10 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
branchRequest.setParentBranch(source.getName());
branchRequest.setNewBranch("new_branch");
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest);
createCommand().branch(branchRequest);
Branch newBranch = findBranch(context, "new_branch");
Assertions.assertThat(newBranch.getRevision()).isEqualTo(source.getRevision());
assertThat(newBranch.getRevision()).isEqualTo(source.getRevision());
}
private Branch findBranch(GitContext context, String name) throws IOException {
@@ -41,17 +58,79 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
public void shouldCreateBranch() throws IOException {
GitContext context = createContext();
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
BranchRequest branchRequest = new BranchRequest();
branchRequest.setNewBranch("new_branch");
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest);
createCommand().branch(branchRequest);
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
}
@Test
public void shouldDeleteBranch() throws IOException {
GitContext context = createContext();
String branchToBeDeleted = "squash";
createCommand().deleteOrClose(branchToBeDeleted);
assertThat(readBranches(context)).filteredOn(b -> b.getName().equals(branchToBeDeleted)).isEmpty();
}
@Test
public void shouldThrowExceptionWhenDeletingDefaultBranch() {
String branchToBeDeleted = "master";
assertThrows(CannotDeleteDefaultBranchException.class, () -> createCommand().deleteOrClose(branchToBeDeleted));
}
private GitBranchCommand createCommand() {
return new GitBranchCommand(createContext(), repository, hookContextFactory, eventBus);
}
private List<Branch> readBranches(GitContext context) throws IOException {
return new GitBranchesCommand(context, repository).getBranches();
}
@Test
public void shouldPostCreateEvents() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
BranchRequest branchRequest = new BranchRequest();
branchRequest.setParentBranch("mergeable");
branchRequest.setNewBranch("new_branch");
createCommand().branch(branchRequest);
List<Object> events = captor.getAllValues();
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).containsExactly("new_branch");
assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).isEmpty();
}
@Test
public void shouldPostDeleteEvents() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
createCommand().deleteOrClose("squash");
List<Object> events = captor.getAllValues();
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).containsExactly("squash");
assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).isEmpty();
}
private HookContext createMockedContext(InvocationOnMock invocation) {
HookContext mock = mock(HookContext.class);
when(mock.getBranchProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getBranchProvider());
return mock;
}
}

View File

@@ -35,7 +35,11 @@
package sonia.scm.repository.spi;
import com.google.common.io.Files;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitRepositoryConfig;
@@ -51,14 +55,18 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link GitLogCommand}.
*
* @author Sebastian Sdorra
*/
@RunWith(MockitoJUnitRunner.class)
public class GitLogCommandTest extends AbstractGitCommandTestBase
{
@Mock
LogCommandRequest request;
/**
* Tests log command with the usage of a default branch.
@@ -171,7 +179,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
public void testGetCommit()
{
GitLogCommand command = createCommand();
Changeset c = command.getChangeset("435df2f061add3589cb3");
Changeset c = command.getChangeset("435df2f061add3589cb3", null);
assertNotNull(c);
String revision = "435df2f061add3589cb326cc64be9b9c3897ceca";
@@ -193,6 +201,23 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertThat(modifications.getAdded(), contains("a.txt", "b.txt"));
}
@Test
public void commitShouldContainBranchIfLogCommandRequestHasBranch()
{
when(request.getBranch()).thenReturn("master");
GitLogCommand command = createCommand();
Changeset c = command.getChangeset("435df2f061add3589cb3", request);
Assertions.assertThat(c.getBranches()).containsOnly("master");
}
@Test
public void shouldNotReturnCommitFromDifferentBranch() {
when(request.getBranch()).thenReturn("master");
Changeset changeset = createCommand().getChangeset("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", request);
Assertions.assertThat(changeset).isNull();
}
@Test
public void testGetRange()
{

View File

@@ -15,10 +15,12 @@ import org.junit.Test;
import sonia.scm.NotFoundException;
import sonia.scm.repository.Person;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeStrategy;
import sonia.scm.repository.util.WorkdirProvider;
import sonia.scm.user.User;
import java.io.IOException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -62,6 +64,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -88,6 +91,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("empty_merge");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -109,6 +113,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -132,6 +137,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setMessageTemplate("simple");
@@ -152,6 +158,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("test-branch");
request.setTargetBranch("master");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -173,6 +180,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -192,6 +200,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setTargetBranch("mergeable");
request.setBranchToMerge("master");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
MergeCommandResult mergeCommandResult = command.merge(request);
@@ -211,12 +220,112 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
}
@Test
public void shouldSquashCommitsIfSquashIsEnabled() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setBranchToMerge("squash");
request.setTargetBranch("master");
request.setMessageTemplate("this is a squash");
request.setMergeStrategy(MergeStrategy.SQUASH);
MergeCommandResult mergeCommandResult = command.merge(request);
Repository repository = createContext().open();
assertThat(mergeCommandResult.isSuccess()).isTrue();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
String message = mergeCommit.getFullMessage();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(message).isEqualTo("this is a squash");
}
@Test
public void shouldSquashThreeCommitsIntoOne() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
request.setBranchToMerge("squash");
request.setTargetBranch("master");
request.setMessageTemplate("squash three commits");
request.setMergeStrategy(MergeStrategy.SQUASH);
Repository gitRepository = createContext().open();
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Iterable<RevCommit> commits = new Git(gitRepository).log().add(gitRepository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
String message = mergeCommit.getFullMessage();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(message).isEqualTo("squash three commits");
GitModificationsCommand modificationsCommand = new GitModificationsCommand(createContext(), repository);
List<String> changes = modificationsCommand.getModifications("master").getAdded();
assertThat(changes.size()).isEqualTo(3);
}
@Test
public void shouldMergeWithFastForward() throws IOException, GitAPIException {
Repository repository = createContext().open();
ObjectId featureBranchHead = new Git(repository).log().add(repository.resolve("squash")).setMaxCount(1).call().iterator().next().getId();
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("squash");
request.setTargetBranch("master");
request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
assertThat(mergeCommit.getParentCount()).isEqualTo(1);
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
assertThat(mergeAuthor.getName()).isEqualTo("Philip J Fry");
assertThat(mergeCommit.getId()).isEqualTo(featureBranchHead);
}
@Test
public void shouldDoMergeCommitIfFastForwardIsNotPossible() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setMergeStrategy(MergeStrategy.FAST_FORWARD_IF_POSSIBLE);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
assertThat(mergeCommit.getParentCount()).isEqualTo(2);
String message = mergeCommit.getFullMessage();
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
assertThat(message).contains("master", "mergeable");
}
@Test(expected = NotFoundException.class)
public void shouldHandleNotExistingSourceBranchInMerge() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("mergeable");
request.setBranchToMerge("not_existing");
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
command.merge(request);
}
@@ -225,6 +334,7 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
public void shouldHandleNotExistingTargetBranchInMerge() {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setMergeStrategy(MergeStrategy.MERGE_COMMIT);
request.setTargetBranch("not_existing");
request.setBranchToMerge("master");

View File

@@ -45,7 +45,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
File masterRepo = createRepositoryDirectory();
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
assertThat(workingCopy.getDirectory())
.exists()
@@ -62,7 +62,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
public void shouldCheckoutInitialBranch() {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), "test-branch")) {
try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), "test-branch")) {
assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt"))
.exists()
.isFile()
@@ -75,10 +75,10 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
File firstDirectory;
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
firstDirectory = workingCopy.getDirectory();
}
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
File secondDirectory = workingCopy.getDirectory();
assertThat(secondDirectory).isNotEqualTo(firstDirectory);
}
@@ -89,7 +89,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
File directory;
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
try (WorkingCopy<Repository, Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
directory = workingCopy.getWorkingRepository().getWorkTree();
}
assertThat(directory).doesNotExist();

View File

@@ -0,0 +1,92 @@
package sonia.scm.web.lfs;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.protocolcommand.CommandContext;
import sonia.scm.protocolcommand.CommandInterpreter;
import sonia.scm.protocolcommand.RepositoryContext;
import sonia.scm.protocolcommand.git.GitRepositoryContextResolver;
import sonia.scm.repository.Repository;
import sonia.scm.security.AccessToken;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Optional;
import static java.time.Instant.parse;
import static java.util.Date.from;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class)
class LFSAuthCommandTest {
static final Repository REPOSITORY = new Repository("1", "git", "space", "X");
static final Date EXPIRATION = from(parse("2007-05-03T10:15:30.00Z"));
@Mock
LfsAccessTokenFactory tokenFactory;
@Mock
GitRepositoryContextResolver gitRepositoryContextResolver;
@Mock
ScmConfiguration configuration;
@InjectMocks
LFSAuthCommand lfsAuthCommand;
@BeforeEach
void initAuthorizationToken() {
AccessToken accessToken = mock(AccessToken.class);
lenient().when(this.tokenFactory.createReadAccessToken(REPOSITORY)).thenReturn(accessToken);
lenient().when(this.tokenFactory.createWriteAccessToken(REPOSITORY)).thenReturn(accessToken);
lenient().when(accessToken.getExpiration()).thenReturn(EXPIRATION);
lenient().when(accessToken.compact()).thenReturn("ACCESS_TOKEN");
}
@BeforeEach
void initConfig() {
lenient().when(configuration.getBaseUrl()).thenReturn("http://example.com");
}
@Test
void shouldHandleGitLfsAuthenticate() {
Optional<CommandInterpreter> commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X upload");
assertThat(commandInterpreter).isPresent();
}
@Test
void shouldNotHandleOtherCommands() {
Optional<CommandInterpreter> commandInterpreter = lfsAuthCommand.canHandle("git-lfs-something repo/space/X upload");
assertThat(commandInterpreter).isEmpty();
}
@Test
void shouldExtractRepositoryArgument() {
CommandInterpreter commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X\t upload").get();
assertThat(commandInterpreter.getParsedArgs()).containsOnly("repo/space/X");
}
@Test
void shouldCreateJsonResponse() throws IOException {
CommandInterpreter commandInterpreter = lfsAuthCommand.canHandle("git-lfs-authenticate repo/space/X\t upload").get();
CommandContext commandContext = createCommandContext();
commandInterpreter.getProtocolHandler().handle(commandContext, createRepositoryContext());
assertThat(commandContext.getOutputStream().toString())
.isEqualTo("{\"href\":\"http://example.com/repo/space/X.git/info/lfs/\",\"header\":{\"Authorization\":\"Bearer ACCESS_TOKEN\"},\"expires_at\":\"2007-05-03T10:15:30Z\"}");
}
private CommandContext createCommandContext() {
return new CommandContext(null, null, null, new ByteArrayOutputStream(), null);
}
private RepositoryContext createRepositoryContext() {
return new RepositoryContext(REPOSITORY, null);
}
}

View File

@@ -0,0 +1,98 @@
package sonia.scm.web.lfs;
import org.eclipse.jgit.lfs.lib.LongObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Repository;
import sonia.scm.security.AccessToken;
import sonia.scm.store.BlobStore;
import java.util.Date;
import static java.time.Instant.parse;
import static java.util.Date.from;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.jgit.lfs.lib.LongObjectId.fromString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class ScmBlobLfsRepositoryTest {
static final Repository REPOSITORY = new Repository("1", "git", "space", "X");
static final Date EXPIRATION = from(parse("2007-05-03T10:15:30.00Z"));
static final LongObjectId OBJECT_ID = fromString("976ed944c37cc5d1606af316937edb9d286ecf6c606af316937edb9d286ecf6c");
@Mock
BlobStore blobStore;
@Mock
LfsAccessTokenFactory tokenFactory;
ScmBlobLfsRepository lfsRepository;
@BeforeEach
void initializeLfsRepository() {
lfsRepository = new ScmBlobLfsRepository(REPOSITORY, blobStore, tokenFactory, "http://scm.org/");
}
@BeforeEach
void initAuthorizationToken() {
AccessToken readToken = createToken("READ_TOKEN");
lenient().when(this.tokenFactory.createReadAccessToken(REPOSITORY))
.thenReturn(readToken);
AccessToken writeToken = createToken("WRITE_TOKEN");
lenient().when(this.tokenFactory.createWriteAccessToken(REPOSITORY))
.thenReturn(writeToken);
}
AccessToken createToken(String mockedValue) {
AccessToken accessToken = mock(AccessToken.class);
lenient().when(accessToken.getExpiration()).thenReturn(EXPIRATION);
lenient().when(accessToken.compact()).thenReturn(mockedValue);
return accessToken;
}
@Test
void shouldTakeExpirationFromToken() {
ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID);
assertThat(downloadAction.expires_at).isEqualTo("2007-05-03T10:15:30Z");
}
@Test
void shouldContainReadTokenForDownlo() {
ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID);
assertThat(downloadAction.header.get("Authorization")).isEqualTo("Bearer READ_TOKEN");
}
@Test
void shouldContainWriteTokenForUpload() {
ExpiringAction downloadAction = lfsRepository.getUploadAction(OBJECT_ID, 42L);
assertThat(downloadAction.header.get("Authorization")).isEqualTo("Bearer WRITE_TOKEN");
}
@Test
void shouldContainUrl() {
ExpiringAction downloadAction = lfsRepository.getDownloadAction(OBJECT_ID);
assertThat(downloadAction.href).isEqualTo("http://scm.org/976ed944c37cc5d1606af316937edb9d286ecf6c606af316937edb9d286ecf6c");
}
@Test
void shouldCreateTokenForDownloadActionOnlyOnce() {
lfsRepository.getDownloadAction(OBJECT_ID);
lfsRepository.getDownloadAction(OBJECT_ID);
verify(tokenFactory, times(1)).createReadAccessToken(REPOSITORY);
}
@Test
void shouldCreateTokenForUploadActionOnlyOnce() {
lfsRepository.getUploadAction(OBJECT_ID, 42L);
lfsRepository.getUploadAction(OBJECT_ID, 42L);
verify(tokenFactory, times(1)).createWriteAccessToken(REPOSITORY);
}
}

View File

@@ -11,41 +11,26 @@ import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Created by omilke on 18.05.2017.
*/
public class LfsServletFactoryTest {
private static final String NAMESPACE = "space";
private static final String NAME = "git-lfs-demo";
private static final Repository REPOSITORY = new Repository("", "GIT", NAMESPACE, NAME);
@Test
public void buildBaseUri() {
String repositoryNamespace = "space";
String repositoryName = "git-lfs-demo";
String result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, true));
assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/")));
//result will be with dot-git suffix, ide
result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, false));
public void shouldBuildBaseUri() {
String result = LfsServletFactory.buildBaseUri(REPOSITORY, requestWithUri("git-lfs-demo"));
assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/")));
}
private HttpServletRequest RequestWithUri(String repositoryName, boolean withDotGitSuffix) {
private HttpServletRequest requestWithUri(String repositoryName) {
HttpServletRequest mockedRequest = mock(HttpServletRequest.class);
final String suffix;
if (withDotGitSuffix) {
suffix = ".git";
} else {
suffix = "";
}
//build from valid live request data
when(mockedRequest.getRequestURL()).thenReturn(
new StringBuffer(String.format("http://localhost:8081/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix)));
when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix));
new StringBuffer(String.format("http://localhost:8081/scm/repo/%s/info/lfs/objects/batch", repositoryName)));
when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/repo/%s/info/lfs/objects/batch", repositoryName));
when(mockedRequest.getContextPath()).thenReturn("/scm");
return mockedRequest;