mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-21 15:59:48 +01:00
merge 2.0.0-m3
This commit is contained in:
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user