mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-22 00:09:47 +01:00
Merge with 2.0.0-m3
This commit is contained in:
@@ -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> {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 <conflict file>
|
||||
</code>
|
||||
<code>git add <conflict file></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);
|
||||
@@ -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 (
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
31
scm-plugins/scm-git-plugin/src/main/js/index.test.ts
Normal file
31
scm-plugins/scm-git-plugin/src/main/js/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user