mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-08 10:41:32 +02:00
merge
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
# ignore everything except scm-server.tar.gz
|
||||
**
|
||||
!scm-server/target/*.tar.gz
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM openjdk:8u171-alpine3.8
|
||||
|
||||
ENV SCM_HOME=/var/lib/scm
|
||||
|
||||
RUN set -x \
|
||||
&& apk add --no-cache mercurial bash \
|
||||
&& addgroup -S -g 1000 scm \
|
||||
&& adduser -S -s /bin/false -G scm -h /opt/scm-server -D -H -u 1000 scm \
|
||||
&& mkdir ${SCM_HOME} \
|
||||
&& chown scm:scm ${SCM_HOME}
|
||||
|
||||
ADD scm-server/target/scm-server-app.tar.gz /opt
|
||||
RUN chown -R scm:scm /opt/scm-server
|
||||
|
||||
WORKDIR /opt/scm-server
|
||||
VOLUME [ "${SCM_HOME}", "/opt/scm-server/var/log" ]
|
||||
EXPOSE 8080
|
||||
USER scm
|
||||
|
||||
ENTRYPOINT [ "/opt/scm-server/bin/scm-server" ]
|
||||
39
Jenkinsfile
vendored
39
Jenkinsfile
vendored
@@ -4,14 +4,15 @@
|
||||
@Library('github.com/cloudogu/ces-build-lib@59d3e94')
|
||||
import com.cloudogu.ces.cesbuildlib.*
|
||||
|
||||
node() { // No specific label
|
||||
node('docker') {
|
||||
|
||||
// Change this as when we go back to default - necessary for proper SonarQube analysis
|
||||
mainBranch = "2.0.0-m3"
|
||||
|
||||
properties([
|
||||
// Keep only the last 10 build to preserve space
|
||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||
buildDiscarder(logRotator(numToKeepStr: '10')),
|
||||
disableConcurrentBuilds()
|
||||
])
|
||||
|
||||
timeout(activity: true, time: 20, unit: 'MINUTES') {
|
||||
@@ -44,6 +45,26 @@ node() { // No specific label
|
||||
currentBuild.result = 'UNSTABLE'
|
||||
}
|
||||
}
|
||||
|
||||
def commitHash = getCommitHash()
|
||||
def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}"
|
||||
|
||||
if (isMainBranch()) {
|
||||
stage('Docker') {
|
||||
def image = docker.build('cloudogu/scm-manager')
|
||||
docker.withRegistry('', 'hub.docker.com-cesmarvin') {
|
||||
image.push(dockerImageTag)
|
||||
image.push('latest')
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deployment') {
|
||||
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
|
||||
string(name: 'changeset', value: commitHash),
|
||||
string(name: 'imageTag', value: dockerImageTag)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Archive Unit and integration test results, if any
|
||||
@@ -62,7 +83,7 @@ Maven setupMavenBuild() {
|
||||
// Keep this version number in sync with .mvn/maven-wrapper.properties
|
||||
Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8")
|
||||
|
||||
if (mainBranch.equals(env.BRANCH_NAME)) {
|
||||
if (isMainBranch()) {
|
||||
// Release starts javadoc, which takes very long, so do only for certain branches
|
||||
mvn.additionalArgs += ' -DperformRelease'
|
||||
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet.
|
||||
@@ -89,7 +110,7 @@ void analyzeWith(Maven mvn) {
|
||||
"-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager "
|
||||
} else {
|
||||
mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} "
|
||||
if (!mainBranch.equals(env.BRANCH_NAME)) {
|
||||
if (!isMainBranch()) {
|
||||
// Avoid exception "The main branch must not have a target" on main branch
|
||||
mvnArgs += " -Dsonar.branch.target=${mainBranch} "
|
||||
}
|
||||
@@ -98,9 +119,13 @@ void analyzeWith(Maven mvn) {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isMainBranch() {
|
||||
return mainBranch.equals(env.BRANCH_NAME)
|
||||
}
|
||||
|
||||
boolean waitForQualityGateWebhookToBeCalled() {
|
||||
boolean isQualityGateSucceeded = true
|
||||
timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example
|
||||
timeout(time: 5, unit: 'MINUTES') { // Needed when there is no webhook for example
|
||||
def qGate = waitForQualityGate()
|
||||
echo "SonarQube Quality Gate status: ${qGate.status}"
|
||||
if (qGate.status != 'OK') {
|
||||
@@ -114,6 +139,10 @@ String getCommitAuthorComplete() {
|
||||
new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{author}"'
|
||||
}
|
||||
|
||||
String getCommitHash() {
|
||||
new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{node}"'
|
||||
}
|
||||
|
||||
String getCommitAuthorEmail() {
|
||||
def matcher = getCommitAuthorComplete() =~ "<(.*?)>"
|
||||
matcher ? matcher[0][1] : ""
|
||||
|
||||
21
deployments/helm/.helmignore
Normal file
21
deployments/helm/.helmignore
Normal file
@@ -0,0 +1,21 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
5
deployments/helm/Chart.yaml
Normal file
5
deployments/helm/Chart.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
appVersion: "1.0"
|
||||
description: A Helm chart for SCM-Manager
|
||||
name: scm-manager
|
||||
version: 0.1.0
|
||||
19
deployments/helm/templates/NOTES.txt
Normal file
19
deployments/helm/templates/NOTES.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "scm-manager.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get svc -w {{ include "scm-manager.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "scm-manager.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ include "scm-manager.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl port-forward $POD_NAME 8080:80
|
||||
{{- end }}
|
||||
32
deployments/helm/templates/_helpers.tpl
Normal file
32
deployments/helm/templates/_helpers.tpl
Normal file
@@ -0,0 +1,32 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "scm-manager.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "scm-manager.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "scm-manager.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
160
deployments/helm/templates/configmap.yaml
Normal file
160
deployments/helm/templates/configmap.yaml
Normal file
@@ -0,0 +1,160 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: "{{ .Release.Name }}"
|
||||
heritage: "{{ .Release.Service }}"
|
||||
data:
|
||||
server-config.xml: |
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
|
||||
<Configure id="ScmServer" class="org.eclipse.jetty.server.Server">
|
||||
|
||||
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
|
||||
<!-- increase header size for mercurial -->
|
||||
<Set name="requestHeaderSize">16384</Set>
|
||||
<Set name="responseHeaderSize">16384</Set>
|
||||
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
<!--
|
||||
We have to enable ForwardedRequestCustomizer in order to understand X-Forwarded-xxx headers.
|
||||
Without the ForwardedRequestCustomizer, scm will possibly generate wrong links
|
||||
-->
|
||||
<Call name="addCustomizer">
|
||||
<Arg><New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/></Arg>
|
||||
</Call>
|
||||
{{- end }}
|
||||
</New>
|
||||
|
||||
<!--
|
||||
Connectors
|
||||
-->
|
||||
<Call name="addConnector">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.server.ServerConnector">
|
||||
<Arg name="server">
|
||||
<Ref refid="ScmServer" />
|
||||
</Arg>
|
||||
<Arg name="factories">
|
||||
<Array type="org.eclipse.jetty.server.ConnectionFactory">
|
||||
<Item>
|
||||
<New class="org.eclipse.jetty.server.HttpConnectionFactory">
|
||||
<Arg name="config">
|
||||
<Ref refid="httpConfig" />
|
||||
</Arg>
|
||||
</New>
|
||||
</Item>
|
||||
</Array>
|
||||
</Arg>
|
||||
<Set name="port">
|
||||
<SystemProperty name="jetty.port" default="8080" />
|
||||
</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
|
||||
<New id="scm-webapp" class="org.eclipse.jetty.webapp.WebAppContext">
|
||||
<Set name="contextPath">/scm</Set>
|
||||
<Set name="war">
|
||||
<SystemProperty name="basedir" default="."/>/var/webapp/scm-webapp.war</Set>
|
||||
<!-- disable directory listings -->
|
||||
<Call name="setInitParameter">
|
||||
<Arg>org.eclipse.jetty.servlet.Default.dirAllowed</Arg>
|
||||
<Arg>false</Arg>
|
||||
</Call>
|
||||
<Set name="tempDirectory">
|
||||
<SystemProperty name="basedir" default="."/>/work/scm
|
||||
</Set>
|
||||
</New>
|
||||
|
||||
<New id="docroot" class="org.eclipse.jetty.webapp.WebAppContext">
|
||||
<Set name="contextPath">/</Set>
|
||||
<Set name="baseResource">
|
||||
<New class="org.eclipse.jetty.util.resource.ResourceCollection">
|
||||
<Arg>
|
||||
<Array type="java.lang.String">
|
||||
<Item>
|
||||
<SystemProperty name="basedir" default="."/>/var/webapp/docroot</Item>
|
||||
</Array>
|
||||
</Arg>
|
||||
</New>
|
||||
</Set>
|
||||
<Set name="tempDirectory">
|
||||
<SystemProperty name="basedir" default="."/>/work/docroot
|
||||
</Set>
|
||||
</New>
|
||||
|
||||
<Set name="handler">
|
||||
<New class="org.eclipse.jetty.server.handler.HandlerCollection">
|
||||
<Set name="handlers">
|
||||
<Array type="org.eclipse.jetty.server.Handler">
|
||||
<Item>
|
||||
<Ref id="scm-webapp" />
|
||||
</Item>
|
||||
<Item>
|
||||
<Ref id="docroot" />
|
||||
</Item>
|
||||
</Array>
|
||||
</Set>
|
||||
</New>
|
||||
</Set>
|
||||
|
||||
</Configure>
|
||||
|
||||
logging.xml: |
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<--
|
||||
in a container environment we only need stdout
|
||||
-->
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
|
||||
</appender>
|
||||
|
||||
<logger name="sonia.scm" level="INFO" />
|
||||
|
||||
<!-- suppress massive gzip logging -->
|
||||
<logger name="sonia.scm.filter.GZipFilter" level="WARN" />
|
||||
<logger name="sonia.scm.filter.GZipResponseStream" level="WARN" />
|
||||
|
||||
<logger name="sonia.scm.util.ServiceUtil" level="WARN" />
|
||||
|
||||
<!-- event bus -->
|
||||
<logger name="sonia.scm.event.LegmanScmEventBus" level="INFO" />
|
||||
|
||||
<!-- shiro -->
|
||||
<!--
|
||||
<logger name="org.apache.shiro" level="INFO" />
|
||||
<logger name="org.apache.shiro.authc.pam.ModularRealmAuthenticator" level="DEBUG" />
|
||||
-->
|
||||
|
||||
<!-- svnkit -->
|
||||
<!--
|
||||
<logger name="svnkit" level="WARN" />
|
||||
<logger name="svnkit.network" level="DEBUG" />
|
||||
<logger name="svnkit.fsfs" level="WARN" />
|
||||
-->
|
||||
|
||||
<!-- javahg -->
|
||||
<!--
|
||||
<logger name="com.aragost.javahg" level="DEBUG" />
|
||||
-->
|
||||
|
||||
<!-- ehcache -->
|
||||
<!--
|
||||
<logger name="net.sf.ehcache" level="DEBUG" />
|
||||
-->
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
77
deployments/helm/templates/deployment.yaml
Normal file
77
deployments/helm/templates/deployment.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: {{ include "scm-manager.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: 1 # could not be scaled
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
initContainers:
|
||||
- name: volume-permissions
|
||||
image: alpine:3.8
|
||||
imagePullPolicy: IfNotPresent
|
||||
command: ['sh', '-c', 'chown 1000:1000 /data']
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /scm
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /scm
|
||||
port: http
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/scm
|
||||
- name: config
|
||||
mountPath: /opt/scm-server/conf
|
||||
volumes:
|
||||
- name: data
|
||||
{{- if .Values.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "scm-manager.fullname" . }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
38
deployments/helm/templates/ingress.yaml
Normal file
38
deployments/helm/templates/ingress.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "scm-manager.fullname" . -}}
|
||||
{{- $ingressPath := .Values.ingress.path -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: {{ include "scm-manager.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{ toYaml . | indent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ $ingressPath }}
|
||||
backend:
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
24
deployments/helm/templates/pvc.yaml
Normal file
24
deployments/helm/templates/pvc.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
{{- if .Values.persistence.enabled -}}
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: "{{ .Release.Name }}"
|
||||
heritage: "{{ .Release.Service }}"
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.persistence.accessMode | quote }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size | quote }}
|
||||
{{- if .Values.persistence.storageClass }}
|
||||
{{- if (eq "-" .Values.persistence.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.persistence.storageClass }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
19
deployments/helm/templates/service.yaml
Normal file
19
deployments/helm/templates/service.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "scm-manager.fullname" . }}
|
||||
labels:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
chart: {{ include "scm-manager.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: {{ include "scm-manager.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
65
deployments/helm/values.yaml
Normal file
65
deployments/helm/values.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
# Default values for scm-manager.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: cloudogu/scm-manager
|
||||
# TODO change after release, to something more stable
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
service:
|
||||
type: LoadBalancer
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- scm-manager.local
|
||||
tls: []
|
||||
# - secretName: scm-manager-tls
|
||||
# hosts:
|
||||
# - scm-manager.local
|
||||
|
||||
## Enable persistence using Persistent Volume Claims
|
||||
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
|
||||
##
|
||||
persistence:
|
||||
enabled: true
|
||||
## ghost data Persistent Volume Storage Class
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||
## GKE, AWS & OpenStack)
|
||||
##
|
||||
# storageClass: "-"
|
||||
accessMode: ReadWriteOnce
|
||||
size: 12Gi
|
||||
|
||||
resources:
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2048Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 256Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
7
pom.xml
7
pom.xml
@@ -188,15 +188,14 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-lib</artifactId>
|
||||
<version>${ssp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-processor</artifactId>
|
||||
<version>${ssp.version}</version>
|
||||
<optional>true</optional>
|
||||
@@ -765,7 +764,7 @@
|
||||
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
|
||||
|
||||
<!-- security libraries -->
|
||||
<ssp.version>967c8fd521</ssp.version>
|
||||
<ssp.version>1.1.0</ssp.version>
|
||||
<shiro.version>1.4.0</shiro.version>
|
||||
|
||||
<!-- repostitory libraries -->
|
||||
|
||||
@@ -94,6 +94,12 @@
|
||||
<artifactId>javax.ws.rs-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- json -->
|
||||
|
||||
<dependency>
|
||||
@@ -160,14 +166,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-lib</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
|
||||
<groupId>com.github.sdorra</groupId>
|
||||
<artifactId>ssp-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
@@ -4,11 +4,11 @@ import java.net.URI;
|
||||
|
||||
public interface ScmPathInfo {
|
||||
|
||||
String REST_API_PATH = "/api/rest";
|
||||
String REST_API_PATH = "/api";
|
||||
|
||||
URI getApiRestUri();
|
||||
|
||||
default URI getRootUri() {
|
||||
return getApiRestUri().resolve("../..");
|
||||
return getApiRestUri().resolve("..");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
|
||||
@StaticPermissions(
|
||||
value = "configuration",
|
||||
permissions = {"read", "write"},
|
||||
globalPermissions = {}
|
||||
globalPermissions = {"list"}
|
||||
)
|
||||
public interface Configuration extends PermissionObject {
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.filter;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.util.WebUtil;
|
||||
import sonia.scm.web.filter.HttpFilter;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Filter for gzip encoding.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.15
|
||||
*/
|
||||
@Priority(Filters.PRIORITY_PRE_BASEURL)
|
||||
@WebElement(value = Filters.PATTERN_RESOURCE_REGEX, regex = true)
|
||||
public class GZipFilter extends HttpFilter
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for GZipFilter
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GZipFilter.class);
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the configuration for the gzip filter.
|
||||
*
|
||||
*
|
||||
* @return gzip filter configuration
|
||||
*/
|
||||
public GZipFilterConfig getConfig()
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Encodes the response, if the request has support for gzip encoding.
|
||||
*
|
||||
*
|
||||
* @param request http request
|
||||
* @param response http response
|
||||
* @param chain filter chain
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
if (WebUtil.isGzipSupported(request))
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("compress output with gzip");
|
||||
}
|
||||
|
||||
GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response,
|
||||
config);
|
||||
|
||||
chain.doFilter(request, wrappedResponse);
|
||||
wrappedResponse.finishResponse();
|
||||
}
|
||||
else
|
||||
{
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** gzip filter configuration */
|
||||
private GZipFilterConfig config = new GZipFilterConfig();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package sonia.scm.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.util.WebUtil;
|
||||
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerResponseContext;
|
||||
import javax.ws.rs.container.ContainerResponseFilter;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
@Provider
|
||||
@Slf4j
|
||||
public class GZipResponseFilter implements ContainerResponseFilter {
|
||||
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
|
||||
if (WebUtil.isGzipSupported(requestContext::getHeaderString)) {
|
||||
log.trace("compress output with gzip");
|
||||
GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream());
|
||||
responseContext.getHeaders().add("Content-Encoding", "gzip");
|
||||
responseContext.setEntityStream(wrappedResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ import java.util.List;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions("group")
|
||||
@StaticPermissions(value = "group", globalPermissions = {"create", "list"})
|
||||
@XmlRootElement(name = "groups")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class Group extends BasicPropertiesAware
|
||||
|
||||
@@ -82,6 +82,22 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
|
||||
{
|
||||
this.total = total;
|
||||
this.changesets = changesets;
|
||||
this.branchName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new changeset paging result for a specific branch.
|
||||
*
|
||||
*
|
||||
* @param total total number of changesets
|
||||
* @param changesets current list of fetched changesets
|
||||
* @param branchName branch name this result was created for
|
||||
*/
|
||||
public ChangesetPagingResult(int total, List<Changeset> changesets, String branchName)
|
||||
{
|
||||
this.total = total;
|
||||
this.changesets = changesets;
|
||||
this.branchName = branchName;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -158,6 +174,7 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("changesets", changesets)
|
||||
.add("total", total)
|
||||
.add("branch", branchName)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
@@ -186,37 +203,35 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
|
||||
return total;
|
||||
}
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets the current list of changesets.
|
||||
*
|
||||
*
|
||||
* @param changesets current list of changesets
|
||||
*/
|
||||
public void setChangesets(List<Changeset> changesets)
|
||||
void setChangesets(List<Changeset> changesets)
|
||||
{
|
||||
this.changesets = changesets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of changesets
|
||||
*
|
||||
*
|
||||
* @param total total number of changesets
|
||||
*/
|
||||
public void setTotal(int total)
|
||||
void setTotal(int total)
|
||||
{
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
void setBranchName(String branchName) {
|
||||
this.branchName = branchName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the branch name this result was created for. This can either be an explicit branch ("give me all
|
||||
* changesets for branch xyz") or an implicit one ("give me the changesets for the default").
|
||||
*/
|
||||
public String getBranchName() {
|
||||
return branchName;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** current list of changesets */
|
||||
@XmlElement(name = "changeset")
|
||||
@XmlElementWrapper(name = "changesets")
|
||||
private List<Changeset> changesets;
|
||||
|
||||
/** total number of changesets */
|
||||
private int total;
|
||||
|
||||
private String branchName;
|
||||
}
|
||||
|
||||
@@ -55,11 +55,10 @@ import java.security.Principal;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions("user")
|
||||
@StaticPermissions(value = "user", globalPermissions = {"create", "list"})
|
||||
@XmlRootElement(name = "users")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class
|
||||
User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
|
||||
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
|
||||
@@ -49,6 +49,7 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -266,7 +267,12 @@ public final class WebUtil
|
||||
*/
|
||||
public static boolean isGzipSupported(HttpServletRequest request)
|
||||
{
|
||||
String enc = request.getHeader(HEADER_ACCEPTENCODING);
|
||||
return isGzipSupported(request::getHeader);
|
||||
}
|
||||
|
||||
public static boolean isGzipSupported(Function<String, String> headerResolver)
|
||||
{
|
||||
String enc = headerResolver.apply(HEADER_ACCEPTENCODING);
|
||||
|
||||
return (enc != null) && enc.contains("gzip");
|
||||
}
|
||||
|
||||
40
scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
Normal file
40
scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class JsonEnricherBase implements JsonEnricher {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
protected JsonEnricherBase(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) {
|
||||
return mediaType.equals(context.getResponseMediaType().toString());
|
||||
}
|
||||
|
||||
protected JsonNode value(Object object) {
|
||||
return objectMapper.convertValue(object, JsonNode.class);
|
||||
}
|
||||
|
||||
protected ObjectNode createObject() {
|
||||
return objectMapper.createObjectNode();
|
||||
}
|
||||
|
||||
protected ObjectNode createObject(Map<String, Object> values) {
|
||||
ObjectNode object = createObject();
|
||||
|
||||
values.forEach((key, value) -> object.set(key, value(value)));
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) {
|
||||
((ObjectNode) parent).set(newKey, child);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class VndMediaType {
|
||||
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
|
||||
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION;
|
||||
|
||||
public static final String INDEX = PREFIX + "index" + SUFFIX;
|
||||
public static final String USER = PREFIX + "user" + SUFFIX;
|
||||
public static final String GROUP = PREFIX + "group" + SUFFIX;
|
||||
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
|
||||
|
||||
@@ -114,7 +114,7 @@ public class InitializingHttpScmProtocolWrapperTest {
|
||||
}
|
||||
|
||||
private OngoingStubbing<ScmPathInfo> mockSetPathInfo() {
|
||||
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/"));
|
||||
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class JsonEnricherBaseTest {
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper);
|
||||
|
||||
@Test
|
||||
public void testResultHasMediaType() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null);
|
||||
|
||||
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue();
|
||||
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppendLink() {
|
||||
ObjectNode root = objectMapper.createObjectNode();
|
||||
ObjectNode links = objectMapper.createObjectNode();
|
||||
root.set("_links", links);
|
||||
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root);
|
||||
enricher.enrich(context);
|
||||
|
||||
assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link");
|
||||
}
|
||||
|
||||
private static class TestJsonEnricher extends JsonEnricherBase {
|
||||
|
||||
public TestJsonEnricher(ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link")));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
scm-it/src/test/java/sonia/scm/it/IndexITCase.java
Normal file
48
scm-it/src/test/java/sonia/scm/it/IndexITCase.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package sonia.scm.it;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.it.utils.RestUtil;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
|
||||
public class IndexITCase {
|
||||
|
||||
@Test
|
||||
public void shouldLinkEverythingForAdmin() {
|
||||
given(VndMediaType.INDEX)
|
||||
|
||||
.when()
|
||||
.get(RestUtil.createResourceUrl(""))
|
||||
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.body(
|
||||
"_links.repositories.href", matchesPattern(".+/repositories/"),
|
||||
"_links.users.href", matchesPattern(".+/users/"),
|
||||
"_links.groups.href", matchesPattern(".+/groups/"),
|
||||
"_links.config.href", matchesPattern(".+/config"),
|
||||
"_links.gitConfig.href", matchesPattern(".+/config/git"),
|
||||
"_links.hgConfig.href", matchesPattern(".+/config/hg"),
|
||||
"_links.svnConfig.href", matchesPattern(".+/config/svn")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateLoginLinksForAnonymousAccess() {
|
||||
RestAssured.given() // do not specify user credentials
|
||||
|
||||
.when()
|
||||
.get(RestUtil.createResourceUrl(""))
|
||||
|
||||
.then()
|
||||
.statusCode(HttpStatus.SC_OK)
|
||||
.body(
|
||||
"_links.login.href", matchesPattern(".+/auth/.+")
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -62,18 +62,4 @@ public class MeITCase {
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
.assertPasswordLinkDoesNotExists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet403IfUserIsNotAdmin() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "xml";
|
||||
TestData.createUser(newUser, password, false, type);
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getMeResource()
|
||||
.assertStatusCode(403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ package sonia.scm.it;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
@@ -93,21 +93,4 @@ public class UserITCase {
|
||||
.assertType(s -> assertThat(s).isEqualTo(type))
|
||||
.assertPasswordLinkDoesNotExists();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet403IfUserIsNotAdmin() {
|
||||
String newUser = "user";
|
||||
String password = "pass";
|
||||
String type = "xml";
|
||||
TestData.createUser(newUser, password, false, type);
|
||||
ScmRequests.start()
|
||||
.given()
|
||||
.url(TestData.getMeUrl())
|
||||
.usernameAndPassword(newUser, password)
|
||||
.getUserResource()
|
||||
.assertStatusCode(403);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ public class RegExMatcher extends BaseMatcher<String> {
|
||||
|
||||
@Override
|
||||
public boolean matches(Object o) {
|
||||
return Pattern.compile(pattern).matcher(o.toString()).matches();
|
||||
return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import static java.net.URI.create;
|
||||
public class RestUtil {
|
||||
|
||||
public static final URI BASE_URL = create("http://localhost:8081/scm/");
|
||||
public static final URI REST_BASE_URL = BASE_URL.resolve("api/rest/v2/");
|
||||
public static final URI REST_BASE_URL = BASE_URL.resolve("api/v2/");
|
||||
|
||||
public static URI createResourceUrl(String path) {
|
||||
return REST_BASE_URL.resolve(path);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class GitConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public GitConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||
@@ -51,8 +50,8 @@ import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -130,27 +129,9 @@ public class GitChangesetConverter implements Closeable
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public Changeset createChangeset(RevCommit commit) throws IOException
|
||||
public Changeset createChangeset(RevCommit commit)
|
||||
{
|
||||
List<String> branches = Lists.newArrayList();
|
||||
Set<Ref> refs = repository.getAllRefsByPeeledObjectId().get(commit.getId());
|
||||
|
||||
if (Util.isNotEmpty(refs))
|
||||
{
|
||||
|
||||
for (Ref ref : refs)
|
||||
{
|
||||
String branch = GitUtil.getBranch(ref);
|
||||
|
||||
if (branch != null)
|
||||
{
|
||||
branches.add(branch);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return createChangeset(commit, branches);
|
||||
return createChangeset(commit, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +146,6 @@ public class GitChangesetConverter implements Closeable
|
||||
* @throws IOException
|
||||
*/
|
||||
public Changeset createChangeset(RevCommit commit, String branch)
|
||||
throws IOException
|
||||
{
|
||||
return createChangeset(commit, Lists.newArrayList(branch));
|
||||
}
|
||||
@@ -183,7 +163,6 @@ public class GitChangesetConverter implements Closeable
|
||||
* @throws IOException
|
||||
*/
|
||||
public Changeset createChangeset(RevCommit commit, List<String> branches)
|
||||
throws IOException
|
||||
{
|
||||
String id = commit.getId().name();
|
||||
List<String> parentList = null;
|
||||
|
||||
@@ -63,8 +63,11 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static java.util.Optional.of;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -345,12 +348,11 @@ public final class GitUtil
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public static ObjectId getBranchId(org.eclipse.jgit.lib.Repository repo,
|
||||
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
|
||||
String branchName)
|
||||
throws IOException
|
||||
{
|
||||
ObjectId branchId = null;
|
||||
|
||||
Ref ref = null;
|
||||
if (!branchName.startsWith(REF_HEAD))
|
||||
{
|
||||
branchName = PREFIX_HEADS.concat(branchName);
|
||||
@@ -360,24 +362,19 @@ public final class GitUtil
|
||||
|
||||
try
|
||||
{
|
||||
Ref ref = repo.findRef(branchName);
|
||||
ref = repo.findRef(branchName);
|
||||
|
||||
if (ref != null)
|
||||
{
|
||||
branchId = ref.getObjectId();
|
||||
}
|
||||
else if (logger.isWarnEnabled())
|
||||
if (ref == null)
|
||||
{
|
||||
logger.warn("could not find branch for {}", branchName);
|
||||
}
|
||||
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger.warn("error occured during resolve of branch id", ex);
|
||||
}
|
||||
|
||||
return branchId;
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -499,68 +496,48 @@ public final class GitUtil
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param repo
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo)
|
||||
throws IOException
|
||||
{
|
||||
ObjectId id = null;
|
||||
String head = null;
|
||||
Map<String, Ref> refs = repo.getAllRefs();
|
||||
public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo) {
|
||||
return getRepositoryHeadRef(repo).map(Ref::getObjectId).orElse(null);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Ref> e : refs.entrySet())
|
||||
{
|
||||
String key = e.getKey();
|
||||
public static Optional<Ref> getRepositoryHeadRef(org.eclipse.jgit.lib.Repository repo) {
|
||||
Optional<Ref> foundRef = findMostAppropriateHead(repo.getAllRefs());
|
||||
|
||||
if (REF_HEAD.equals(key))
|
||||
{
|
||||
head = REF_HEAD;
|
||||
id = e.getValue().getObjectId();
|
||||
|
||||
break;
|
||||
}
|
||||
else if (key.startsWith(REF_HEAD_PREFIX))
|
||||
{
|
||||
id = e.getValue().getObjectId();
|
||||
head = key.substring(REF_HEAD_PREFIX.length());
|
||||
|
||||
if (REF_MASTER.equals(head))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (foundRef.isPresent()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("use {}:{} as repository head for directory {}",
|
||||
foundRef.map(GitUtil::getBranch).orElse(null),
|
||||
foundRef.map(Ref::getObjectId).map(ObjectId::name).orElse(null),
|
||||
repo.getDirectory());
|
||||
}
|
||||
} else {
|
||||
logger.warn("could not find repository head in directory {}", repo.getDirectory());
|
||||
}
|
||||
|
||||
if (id == null)
|
||||
{
|
||||
id = repo.resolve(Constants.HEAD);
|
||||
return foundRef;
|
||||
}
|
||||
|
||||
private static Optional<Ref> findMostAppropriateHead(Map<String, Ref> refs) {
|
||||
Ref refHead = refs.get(REF_HEAD);
|
||||
if (refHead != null && refHead.isSymbolic() && isBranch(refHead.getTarget().getName())) {
|
||||
return of(refHead.getTarget());
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
if ((head != null) && (id != null))
|
||||
{
|
||||
logger.debug("use {}:{} as repository head", head, id.name());
|
||||
}
|
||||
else if (id != null)
|
||||
{
|
||||
logger.debug("use {} as repository head", id.name());
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("could not find repository head");
|
||||
}
|
||||
Ref master = refs.get(REF_HEAD_PREFIX + REF_MASTER);
|
||||
if (master != null) {
|
||||
return of(master);
|
||||
}
|
||||
|
||||
return id;
|
||||
Ref develop = refs.get(REF_HEAD_PREFIX + "develop");
|
||||
if (develop != null) {
|
||||
return of(develop);
|
||||
}
|
||||
|
||||
return refs.entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getKey().startsWith(REF_HEAD_PREFIX))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -648,7 +625,7 @@ public final class GitUtil
|
||||
|
||||
return tagName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -34,19 +34,20 @@ package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.GitConstants;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -97,27 +98,29 @@ public class AbstractGitCommand
|
||||
}
|
||||
return commit;
|
||||
}
|
||||
|
||||
protected ObjectId getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
|
||||
ObjectId head;
|
||||
if ( Strings.isNullOrEmpty(requestedBranch) ) {
|
||||
head = getDefaultBranch(gitRepository);
|
||||
} else {
|
||||
head = GitUtil.getBranchId(gitRepository, requestedBranch);
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
|
||||
protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException {
|
||||
ObjectId head;
|
||||
String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH);
|
||||
if (!Strings.isNullOrEmpty(defaultBranchName)) {
|
||||
head = GitUtil.getBranchId(gitRepository, defaultBranchName);
|
||||
Ref ref = getBranchOrDefault(gitRepository, null);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
} else {
|
||||
logger.trace("no default branch configured, use repository head as default");
|
||||
head = GitUtil.getRepositoryHead(gitRepository);
|
||||
return ref.getObjectId();
|
||||
}
|
||||
}
|
||||
|
||||
protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
|
||||
if ( Strings.isNullOrEmpty(requestedBranch) ) {
|
||||
String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH);
|
||||
if (!Strings.isNullOrEmpty(defaultBranchName)) {
|
||||
return GitUtil.getBranchId(gitRepository, defaultBranchName);
|
||||
} else {
|
||||
logger.trace("no default branch configured, use repository head as default");
|
||||
Optional<Ref> repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository);
|
||||
return repositoryHeadRef.orElse(null);
|
||||
}
|
||||
} else {
|
||||
return GitUtil.getBranchId(gitRepository, requestedBranch);
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.eclipse.jgit.errors.MissingObjectException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
@@ -170,8 +171,8 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
GitChangesetConverter converter = null;
|
||||
RevWalk revWalk = null;
|
||||
|
||||
try (org.eclipse.jgit.lib.Repository gr = open()) {
|
||||
if (!gr.getAllRefs().isEmpty()) {
|
||||
try (org.eclipse.jgit.lib.Repository repository = open()) {
|
||||
if (!repository.getAllRefs().isEmpty()) {
|
||||
int counter = 0;
|
||||
int start = request.getPagingStart();
|
||||
|
||||
@@ -188,18 +189,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
ObjectId startId = null;
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getStartChangeset())) {
|
||||
startId = gr.resolve(request.getStartChangeset());
|
||||
startId = repository.resolve(request.getStartChangeset());
|
||||
}
|
||||
|
||||
ObjectId endId = null;
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getEndChangeset())) {
|
||||
endId = gr.resolve(request.getEndChangeset());
|
||||
endId = repository.resolve(request.getEndChangeset());
|
||||
}
|
||||
|
||||
revWalk = new RevWalk(gr);
|
||||
revWalk = new RevWalk(repository);
|
||||
|
||||
converter = new GitChangesetConverter(gr, revWalk);
|
||||
converter = new GitChangesetConverter(repository, revWalk);
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getPath())) {
|
||||
revWalk.setTreeFilter(
|
||||
@@ -207,13 +208,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
|
||||
}
|
||||
|
||||
ObjectId head = getBranchOrDefault(gr, request.getBranch());
|
||||
Ref branch = getBranchOrDefault(repository,request.getBranch());
|
||||
|
||||
if (head != null) {
|
||||
if (branch != null) {
|
||||
if (startId != null) {
|
||||
revWalk.markStart(revWalk.lookupCommit(startId));
|
||||
} else {
|
||||
revWalk.markStart(revWalk.lookupCommit(head));
|
||||
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
|
||||
}
|
||||
|
||||
Iterator<RevCommit> iterator = revWalk.iterator();
|
||||
@@ -234,10 +235,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
}
|
||||
}
|
||||
|
||||
changesets = new ChangesetPagingResult(counter, changesetList);
|
||||
if (branch != null) {
|
||||
changesets = new ChangesetPagingResult(counter, changesetList, GitUtil.getBranch(branch.getName()));
|
||||
} else {
|
||||
changesets = new ChangesetPagingResult(counter, changesetList);
|
||||
}
|
||||
} else if (logger.isWarnEnabled()) {
|
||||
logger.warn("the repository {} seems to be empty",
|
||||
repository.getName());
|
||||
this.repository.getName());
|
||||
|
||||
changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
@@ -10,14 +11,16 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
if (!repository._links.httpProtocol) {
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Clone the repository</h4>
|
||||
<pre>
|
||||
<code>git clone {repository._links.httpProtocol.href}</code>
|
||||
<code>git clone {href}</code>
|
||||
</pre>
|
||||
<h4>Create a new repository</h4>
|
||||
<pre>
|
||||
@@ -30,7 +33,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<br />
|
||||
git commit -m "added readme"
|
||||
<br />
|
||||
git remote add origin {repository._links.httpProtocol.href}
|
||||
git remote add origin {href}
|
||||
<br />
|
||||
git push -u origin master
|
||||
<br />
|
||||
@@ -39,7 +42,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<h4>Push an existing repository</h4>
|
||||
<pre>
|
||||
<code>
|
||||
git remote add origin {repository._links.httpProtocol.href}
|
||||
git remote add origin {href}
|
||||
<br />
|
||||
git push -u origin master
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class GitConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final GitConfigInIndexResource gitConfigInIndexResource;
|
||||
|
||||
public GitConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
gitConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra
|
||||
* All rights reserved.
|
||||
@@ -33,14 +34,17 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.GitConstants;
|
||||
import sonia.scm.repository.Modifications;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import static java.nio.charset.Charset.defaultCharset;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -48,8 +52,6 @@ import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GitLogCommand}.
|
||||
*
|
||||
@@ -72,6 +74,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
||||
assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", result.getChangesets().get(1).getId());
|
||||
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(2).getId());
|
||||
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(3).getId());
|
||||
assertEquals("master", result.getBranchName());
|
||||
assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
|
||||
|
||||
// set default branch and fetch again
|
||||
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
|
||||
@@ -79,10 +83,12 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
||||
result = createCommand().getChangesets(new LogCommandRequest());
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("test-branch", result.getBranchName());
|
||||
assertEquals(3, result.getTotal());
|
||||
assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", result.getChangesets().get(0).getId());
|
||||
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(1).getId());
|
||||
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId());
|
||||
assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -210,6 +216,32 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
|
||||
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFindDefaultBranchFromHEAD() throws Exception {
|
||||
setRepositoryHeadReference("ref: refs/heads/test-branch");
|
||||
|
||||
ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest());
|
||||
|
||||
assertEquals("test-branch", changesets.getBranchName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFindMasterBranchWhenHEADisNoRef() throws Exception {
|
||||
setRepositoryHeadReference("592d797cd36432e591416e8b2b98154f4f163411");
|
||||
|
||||
ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest());
|
||||
|
||||
assertEquals("master", changesets.getBranchName());
|
||||
}
|
||||
|
||||
private void setRepositoryHeadReference(String s) throws IOException {
|
||||
Files.write(s, repositoryHeadReferenceFile(), defaultCharset());
|
||||
}
|
||||
|
||||
private File repositoryHeadReferenceFile() {
|
||||
return new File(repositoryDirectory, "HEAD");
|
||||
}
|
||||
|
||||
private GitLogCommand createCommand()
|
||||
{
|
||||
return new GitLogCommand(createContext(), repository);
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:git
|
||||
writer = configuration:write:git
|
||||
readerWriter = configuration:*:git
|
||||
admin = *
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class HgConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public HgConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,11 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
|
||||
List<Changeset> changesets = on(repository).rev(start + ":"
|
||||
+ end).execute();
|
||||
|
||||
result = new ChangesetPagingResult(total, changesets);
|
||||
if (request.getBranch() == null) {
|
||||
result = new ChangesetPagingResult(total, changesets);
|
||||
} else {
|
||||
result = new ChangesetPagingResult(total, changesets, request.getBranch());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -216,10 +216,7 @@ public abstract class AbstractChangesetCommand extends AbstractCommand
|
||||
|
||||
String branch = in.textUpTo('\n');
|
||||
|
||||
if (!BRANCH_DEFAULT.equals(branch))
|
||||
{
|
||||
changeset.getBranches().add(branch);
|
||||
}
|
||||
changeset.getBranches().add(branch);
|
||||
|
||||
String p1 = readId(in, changeset, PROPERTY_PARENT1_REVISION);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
if (!repository._links.httpProtocol) {
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h4>Clone the repository</h4>
|
||||
<pre>
|
||||
<code>hg clone {repository._links.httpProtocol.href}</code>
|
||||
<code>hg clone {href}</code>
|
||||
</pre>
|
||||
<h4>Create a new repository</h4>
|
||||
<pre>
|
||||
@@ -26,7 +28,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<br />
|
||||
echo "[paths]" > .hg/hgrc
|
||||
<br />
|
||||
echo "default = {repository._links.httpProtocol.href}" > .hg/hgrc
|
||||
echo "default = {href}" > .hg/hgrc
|
||||
<br />
|
||||
echo "# {repository.name}" > README.md
|
||||
<br />
|
||||
@@ -44,7 +46,7 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
<code>
|
||||
# add the repository url as default to your .hg/hgrc e.g:
|
||||
<br />
|
||||
default = {repository._links.httpProtocol.href}
|
||||
default = {href}
|
||||
<br />
|
||||
# push to remote repository
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class HgConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final HgConfigInIndexResource hgConfigInIndexResource;
|
||||
|
||||
public HgConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
hgConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,21 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
|
||||
result.getChangesets().get(2).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDefaultBranchInfo() {
|
||||
LogCommandRequest request = new LogCommandRequest();
|
||||
|
||||
request.setPath("a.txt");
|
||||
|
||||
ChangesetPagingResult result = createComamnd().getChangesets(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1,
|
||||
result.getChangesets().get(0).getBranches().size());
|
||||
assertEquals("default",
|
||||
result.getChangesets().get(0).getBranches().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAllWithLimit() {
|
||||
LogCommandRequest request = new LogCommandRequest();
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:hg
|
||||
writer = configuration:write:hg
|
||||
readerWriter = configuration:*:hg
|
||||
admin = *
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import sonia.scm.config.ConfigurationPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.JsonEnricherBase;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static sonia.scm.web.VndMediaType.INDEX;
|
||||
|
||||
@Extension
|
||||
public class SvnConfigInIndexResource extends JsonEnricherBase {
|
||||
|
||||
private final Provider<ScmPathInfoStore> scmPathInfoStore;
|
||||
|
||||
@Inject
|
||||
public SvnConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
this.scmPathInfoStore = scmPathInfoStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enrich(JsonEnricherContext context) {
|
||||
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
|
||||
String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class)
|
||||
.method("get")
|
||||
.parameters()
|
||||
.href();
|
||||
|
||||
JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl)));
|
||||
|
||||
addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,122 +32,45 @@
|
||||
|
||||
package sonia.scm.web;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.filter.GZipFilter;
|
||||
import sonia.scm.filter.GZipFilterConfig;
|
||||
import sonia.scm.filter.GZipResponseWrapper;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.SvnRepositoryHandler;
|
||||
import sonia.scm.repository.spi.ScmProviderHttpServlet;
|
||||
import sonia.scm.util.WebUtil;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class SvnGZipFilter extends GZipFilter implements ScmProviderHttpServlet
|
||||
{
|
||||
class SvnGZipFilter implements ScmProviderHttpServlet {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class);
|
||||
|
||||
private final SvnRepositoryHandler handler;
|
||||
private final ScmProviderHttpServlet delegate;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
private GZipFilterConfig config = new GZipFilterConfig();
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param handler
|
||||
*/
|
||||
public SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate)
|
||||
{
|
||||
SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) {
|
||||
this.handler = handler;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param filterConfig
|
||||
*
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException
|
||||
{
|
||||
super.init(filterConfig);
|
||||
getConfig().setBufferResponse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param chain
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
if (handler.getConfig().isEnabledGZip())
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("encode svn request with gzip");
|
||||
}
|
||||
|
||||
super.doFilter(request, response, chain);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("skip gzip encoding");
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
config.setBufferResponse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
|
||||
if (handler.getConfig().isEnabledGZip())
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("encode svn request with gzip");
|
||||
}
|
||||
|
||||
super.doFilter(request, response, (servletRequest, servletResponse) -> delegate.service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, repository));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("skip gzip encoding");
|
||||
}
|
||||
|
||||
if (handler.getConfig().isEnabledGZip() && WebUtil.isGzipSupported(request)) {
|
||||
logger.trace("compress svn response with gzip");
|
||||
GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, config);
|
||||
delegate.service(request, wrappedResponse, repository);
|
||||
wrappedResponse.finishResponse();
|
||||
} else {
|
||||
logger.trace("skip gzip encoding");
|
||||
delegate.service(request, response, repository);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { repositories } from "@scm-manager/ui-components";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { repository } = this.props;
|
||||
if (!repository._links.httpProtocol) {
|
||||
const href = repositories.getProtocolLinkByType(repository, "http");
|
||||
if (!href) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h4>Checkout the repository</h4>
|
||||
<pre>
|
||||
<code>svn checkout {repository._links.httpProtocol.href}</code>
|
||||
<code>svn checkout {href}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.web.JsonEnricherContext;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||
public class SvnConfigInIndexResourceTest {
|
||||
|
||||
@Rule
|
||||
public final ShiroRule shiroRule = new ShiroRule();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectNode root = objectMapper.createObjectNode();
|
||||
private final SvnConfigInIndexResource svnConfigInIndexResource;
|
||||
|
||||
public SvnConfigInIndexResourceTest() {
|
||||
root.put("_links", objectMapper.createObjectNode());
|
||||
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
|
||||
pathInfoStore.set(() -> URI.create("/"));
|
||||
svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "admin", password = "secret")
|
||||
public void admin() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "readOnly", password = "secret")
|
||||
public void user() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anonymous() {
|
||||
JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
|
||||
|
||||
svnConfigInIndexResource.enrich(context);
|
||||
|
||||
assertFalse(root.get("_links").iterator().hasNext());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
readOnly = secret, reader
|
||||
writeOnly = secret, writer
|
||||
readWrite = secret, readerWriter
|
||||
admin = secret, admin
|
||||
|
||||
[roles]
|
||||
reader = configuration:read:svn
|
||||
writer = configuration:write:svn
|
||||
readerWriter = configuration:*:svn
|
||||
admin = *
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"link": "lerna exec -- yarn link"
|
||||
"link": "lerna exec -- yarn link",
|
||||
"unlink": "lerna exec --no-bail -- yarn unlink"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lerna": "^3.2.1"
|
||||
|
||||
39
scm-ui-components/packages/ui-components/src/Help.js
Normal file
39
scm-ui-components/packages/ui-components/src/Help.js
Normal file
@@ -0,0 +1,39 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
img: {
|
||||
display: "block"
|
||||
},
|
||||
q: {
|
||||
float: "left",
|
||||
paddingLeft: "3px",
|
||||
float: "right"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
message: string,
|
||||
classes: any
|
||||
};
|
||||
|
||||
class Help extends React.Component<Props> {
|
||||
render() {
|
||||
const { message, classes } = this.props;
|
||||
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
|
||||
return (
|
||||
<div
|
||||
className={classNames("tooltip is-tooltip-right", multiline, classes.q)}
|
||||
data-tooltip={message}
|
||||
>
|
||||
<i
|
||||
className={classNames("fa fa-question has-text-info", classes.img)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(Help);
|
||||
@@ -0,0 +1,46 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Help } from "./index";
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class LabelWithHelpIcon extends React.Component<Props> {
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
const helpText = this.props.helpText;
|
||||
if (helpText) {
|
||||
return (
|
||||
<div className="control columns is-vcentered">
|
||||
<Help message={helpText} />
|
||||
</div>
|
||||
);
|
||||
} else return null;
|
||||
};
|
||||
|
||||
renderLabelWithHelpIcon = () => {
|
||||
if (this.props.label) {
|
||||
return (
|
||||
<div className="field is-grouped">
|
||||
<div className="control">{this.renderLabel()}</div>
|
||||
{this.renderHelp()}
|
||||
</div>
|
||||
);
|
||||
} else return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.renderLabelWithHelpIcon();
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelWithHelpIcon;
|
||||
@@ -32,7 +32,7 @@ export function createUrl(url: string) {
|
||||
if (url.indexOf("/") !== 0) {
|
||||
urlWithStartingSlash = "/" + urlWithStartingSlash;
|
||||
}
|
||||
return `${contextPath}/api/rest/v2${urlWithStartingSlash}`;
|
||||
return `${contextPath}/api/v2${urlWithStartingSlash}`;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("create url", () => {
|
||||
});
|
||||
|
||||
it("should add prefix for api", () => {
|
||||
expect(createUrl("/users")).toBe("/api/rest/v2/users");
|
||||
expect(createUrl("users")).toBe("/api/rest/v2/users");
|
||||
expect(createUrl("/users")).toBe("/api/v2/users");
|
||||
expect(createUrl("users")).toBe("/api/v2/users");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,8 @@ type Props = {
|
||||
disabled: boolean,
|
||||
buttonLabel: string,
|
||||
fieldLabel: string,
|
||||
errorMessage: string
|
||||
errorMessage: string,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -25,7 +26,13 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
|
||||
const {
|
||||
disabled,
|
||||
buttonLabel,
|
||||
fieldLabel,
|
||||
errorMessage,
|
||||
helpText
|
||||
} = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<InputField
|
||||
@@ -36,6 +43,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
|
||||
value={this.state.entryToAdd}
|
||||
onReturnPressed={this.appendEntry}
|
||||
disabled={disabled}
|
||||
helpText={helpText}
|
||||
/>
|
||||
<AddButton
|
||||
label={buttonLabel}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Help } from "../index";
|
||||
|
||||
type Props = {
|
||||
label?: string,
|
||||
checked: boolean,
|
||||
onChange?: boolean => void,
|
||||
disabled?: boolean
|
||||
disabled?: boolean,
|
||||
helpText?: string
|
||||
};
|
||||
class Checkbox extends React.Component<Props> {
|
||||
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
@@ -14,9 +16,20 @@ class Checkbox extends React.Component<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
const helpText = this.props.helpText;
|
||||
if (helpText) {
|
||||
return (
|
||||
<div className="control columns is-vcentered">
|
||||
<Help message={helpText} />
|
||||
</div>
|
||||
);
|
||||
} else return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="field">
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<label className="checkbox" disabled={this.props.disabled}>
|
||||
<input
|
||||
@@ -28,6 +41,7 @@ class Checkbox extends React.Component<Props> {
|
||||
{this.props.label}
|
||||
</label>
|
||||
</div>
|
||||
{this.renderHelp()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { LabelWithHelpIcon } from "../index";
|
||||
|
||||
type Props = {
|
||||
label?: string,
|
||||
@@ -12,7 +13,8 @@ type Props = {
|
||||
onReturnPressed?: () => void,
|
||||
validationError: boolean,
|
||||
errorMessage: string,
|
||||
disabled?: boolean
|
||||
disabled?: boolean,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class InputField extends React.Component<Props> {
|
||||
@@ -33,15 +35,6 @@ class InputField extends React.Component<Props> {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
|
||||
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
||||
const onReturnPressed = this.props.onReturnPressed;
|
||||
if (!onReturnPressed) {
|
||||
@@ -60,7 +53,9 @@ class InputField extends React.Component<Props> {
|
||||
value,
|
||||
validationError,
|
||||
errorMessage,
|
||||
disabled
|
||||
disabled,
|
||||
label,
|
||||
helpText
|
||||
} = this.props;
|
||||
const errorView = validationError ? "is-danger" : "";
|
||||
const helper = validationError ? (
|
||||
@@ -70,7 +65,7 @@ class InputField extends React.Component<Props> {
|
||||
);
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<input
|
||||
ref={input => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { LabelWithHelpIcon } from "../index";
|
||||
|
||||
export type SelectItem = {
|
||||
value: string,
|
||||
@@ -10,7 +12,9 @@ type Props = {
|
||||
label?: string,
|
||||
options: SelectItem[],
|
||||
value?: SelectItem,
|
||||
onChange: string => void
|
||||
onChange: string => void,
|
||||
loading?: boolean,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class Select extends React.Component<Props> {
|
||||
@@ -28,21 +32,18 @@ class Select extends React.Component<Props> {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, value } = this.props;
|
||||
const { options, value, label, helpText, loading } = this.props;
|
||||
const loadingClass = loading ? "is-loading" : "";
|
||||
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
<div className="control select">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className={classNames(
|
||||
"control select",
|
||||
loadingClass
|
||||
)}>
|
||||
<select
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { LabelWithHelpIcon } from "../index";
|
||||
|
||||
export type SelectItem = {
|
||||
value: string,
|
||||
@@ -10,7 +11,8 @@ type Props = {
|
||||
label?: string,
|
||||
placeholder?: SelectItem[],
|
||||
value?: string,
|
||||
onChange: string => void
|
||||
onChange: string => void,
|
||||
helpText?: string
|
||||
};
|
||||
|
||||
class Textarea extends React.Component<Props> {
|
||||
@@ -20,20 +22,12 @@ class Textarea extends React.Component<Props> {
|
||||
this.props.onChange(event.target.value);
|
||||
};
|
||||
|
||||
renderLabel = () => {
|
||||
const label = this.props.label;
|
||||
if (label) {
|
||||
return <label className="label">{label}</label>;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
render() {
|
||||
const { placeholder, value } = this.props;
|
||||
const { placeholder, value, label, helpText } = this.props;
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import * as validation from "./validation.js";
|
||||
import * as urls from "./urls";
|
||||
import * as repositories from "./repositories.js";
|
||||
|
||||
export { validation, urls };
|
||||
export { validation, urls, repositories };
|
||||
|
||||
export { default as DateFromNow } from "./DateFromNow.js";
|
||||
export { default as ErrorNotification } from "./ErrorNotification.js";
|
||||
@@ -15,6 +16,8 @@ export { default as MailLink } from "./MailLink.js";
|
||||
export { default as Notification } from "./Notification.js";
|
||||
export { default as Paginator } from "./Paginator.js";
|
||||
export { default as ProtectedRoute } from "./ProtectedRoute.js";
|
||||
export { default as Help } from "./Help.js";
|
||||
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
|
||||
|
||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
||||
|
||||
|
||||
19
scm-ui-components/packages/ui-components/src/repositories.js
Normal file
19
scm-ui-components/packages/ui-components/src/repositories.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @flow
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
// util methods for repositories
|
||||
|
||||
export function getProtocolLinkByType(repository: Repository, type: string) {
|
||||
let protocols = repository._links.protocol;
|
||||
if (protocols) {
|
||||
if (!Array.isArray(protocols)) {
|
||||
protocols = [protocols];
|
||||
}
|
||||
for (let proto of protocols) {
|
||||
if (proto.name === type) {
|
||||
return proto.href;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// @flow
|
||||
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { getProtocolLinkByType, getTypePredicate } from "./repositories";
|
||||
|
||||
describe("getProtocolLinkByType tests", () => {
|
||||
|
||||
it("should return the http protocol link", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: [{
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBe("http://scm.scm-manager.org/repo/scm/core");
|
||||
});
|
||||
|
||||
it("should return the http protocol link from multiple protocols", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: [{
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
},{
|
||||
name: "ssh",
|
||||
href: "git@scm.scm-manager.org:scm/core"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBe("http://scm.scm-manager.org/repo/scm/core");
|
||||
});
|
||||
|
||||
it("should return the http protocol, even if the protocol is a single link", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: {
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBe("http://scm.scm-manager.org/repo/scm/core");
|
||||
});
|
||||
|
||||
it("should return null, if such a protocol does not exists", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {
|
||||
protocol: [{
|
||||
name: "http",
|
||||
href: "http://scm.scm-manager.org/repo/scm/core"
|
||||
},{
|
||||
name: "ssh",
|
||||
href: "git@scm.scm-manager.org:scm/core"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "awesome");
|
||||
expect(link).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null, if no protocols are available", () => {
|
||||
|
||||
const repository: Repository = {
|
||||
namespace: "scm",
|
||||
name: "core",
|
||||
type: "git",
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const link = getProtocolLinkByType(repository, "http");
|
||||
expect(link).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
const nameRegex = /^([A-z0-9.\-_@]|[^ ]([A-z0-9.\-_@ ]*[A-z0-9.\-_@]|[^\s])?)$/;
|
||||
const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/;
|
||||
|
||||
export const isNameValid = (name: string) => {
|
||||
return nameRegex.test(name);
|
||||
|
||||
@@ -5,6 +5,7 @@ describe("test name validation", () => {
|
||||
it("should return false", () => {
|
||||
// invalid names taken from ValidationUtilTest.java
|
||||
const invalidNames = [
|
||||
"@test",
|
||||
" test 123",
|
||||
" test 123 ",
|
||||
"test 123 ",
|
||||
@@ -35,10 +36,9 @@ describe("test name validation", () => {
|
||||
"Test123-git",
|
||||
"Test_user-123.git",
|
||||
"test@scm-manager.de",
|
||||
"test 123",
|
||||
"test123",
|
||||
"tt",
|
||||
"t",
|
||||
|
||||
"valid_name",
|
||||
"another1",
|
||||
"stillValid",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
//@flow
|
||||
import type { Links } from "./hal";
|
||||
|
||||
export type Permission = {
|
||||
name: string,
|
||||
type: string,
|
||||
groupPermission: boolean,
|
||||
_links?: Links
|
||||
};
|
||||
|
||||
export type PermissionEntry = {
|
||||
name: string,
|
||||
type: string,
|
||||
groupPermission: boolean
|
||||
}
|
||||
|
||||
export type PermissionCollection = Permission[];
|
||||
@@ -1,9 +1,10 @@
|
||||
// @flow
|
||||
export type Link = {
|
||||
href: string
|
||||
href: string,
|
||||
name?: string
|
||||
};
|
||||
|
||||
export type Links = { [string]: Link };
|
||||
export type Links = { [string]: Link | Link[] };
|
||||
|
||||
export type Collection = {
|
||||
_embedded: Object,
|
||||
|
||||
@@ -10,3 +10,5 @@ export type { Repository, RepositoryCollection, RepositoryGroup } from "./Reposi
|
||||
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||
|
||||
export type { Config } from "./Config";
|
||||
|
||||
export type { Permission, PermissionEntry, PermissionCollection } from "./RepositoryPermissions";
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
<script>bootstrap</script>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>unlink</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<script>unlink</script>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>link</id>
|
||||
<phase>package</phase>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
"@fortawesome/fontawesome-free": "^5.3.1",
|
||||
"@scm-manager/ui-extensions": "^0.0.7",
|
||||
"bulma": "^0.7.1",
|
||||
"bulma-tooltip": "^2.0.2",
|
||||
"classnames": "^2.2.5",
|
||||
"font-awesome": "^4.7.0",
|
||||
"history": "^4.7.2",
|
||||
@@ -15,6 +16,7 @@
|
||||
"i18next-browser-languagedetector": "^2.2.2",
|
||||
"i18next-fetch-backend": "^0.1.0",
|
||||
"moment": "^2.22.2",
|
||||
"node-sass": "^4.9.3",
|
||||
"react": "^16.4.2",
|
||||
"react-diff-view": "^1.7.0",
|
||||
"react-dom": "^16.4.2",
|
||||
|
||||
@@ -64,5 +64,29 @@
|
||||
"login-attempt-limit-timeout-invalid": "This is not a number",
|
||||
"login-attempt-limit-invalid": "This is not a number",
|
||||
"plugin-url-invalid": "This is not a valid url"
|
||||
},
|
||||
"help": {
|
||||
"realmDescriptionHelpText": "Enter authentication realm description",
|
||||
"dateFormatHelpText": "Moments date format. Please have a look at the momentjs documentation.",
|
||||
"pluginRepositoryHelpText": "The url of the plugin repository. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture",
|
||||
"enableForwardingHelpText": "Enbale mod_proxy port forwarding.",
|
||||
"enableRepositoryArchiveHelpText": "Enable repository archives. A complete page reload is required after a change of this value.",
|
||||
"disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.",
|
||||
"allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.",
|
||||
"skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.",
|
||||
"adminGroupsHelpText": "Names of groups with admin permissions.",
|
||||
"adminUsersHelpText": "Names of users with admin permissions.",
|
||||
"forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url",
|
||||
"baseUrlHelpText": "The url of the application (with context path), i.e. http://localhost:8080/scm",
|
||||
"loginAttemptLimitHelpText": "Maximum allowed login attempts. Use -1 to disable the login attempt limit.",
|
||||
"loginAttemptLimitTimeoutHelpText": "Timeout in seconds for users which are temporary disabled, because of too many failed login attempts.",
|
||||
"enableProxyHelpText": "Enable Proxy",
|
||||
"proxyPortHelpText": "The proxy port",
|
||||
"proxyPasswordHelpText": "The password for the proxy server authentication.",
|
||||
"proxyServerHelpText": "The proxy server",
|
||||
"proxyUserHelpText": "The username for the proxy server authentication.",
|
||||
"proxyExcludesHelpText": "Glob patterns for hostnames which should be excluded from proxy settings.",
|
||||
"enableXsrfProtectionHelpText": "Enable Xsrf Cookie Protection. Note: This feature is still experimental.",
|
||||
"defaultNameSpaceStrategyHelpText": "The default namespace strategy"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,12 @@
|
||||
"group-form": {
|
||||
"submit": "Submit",
|
||||
"name-error": "Group name is invalid",
|
||||
"description-error": "Description is invalid"
|
||||
"description-error": "Description is invalid",
|
||||
"help": {
|
||||
"nameHelpText": "Unique name of the group",
|
||||
"descriptionHelpText": "A short description of the group",
|
||||
"memberHelpText": "Usernames of the group members"
|
||||
}
|
||||
},
|
||||
"delete-group-button": {
|
||||
"label": "Delete",
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"actions-label": "Actions",
|
||||
"back-label": "Back",
|
||||
"navigation-label": "Navigation",
|
||||
"information": "Information"
|
||||
"information": "Information",
|
||||
"permissions": "Permissions"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Repository",
|
||||
@@ -42,5 +43,36 @@
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown permissions error",
|
||||
"name": "User or Group",
|
||||
"type": "Type",
|
||||
"group-permission": "Group Permission",
|
||||
"edit-permission": {
|
||||
"delete-button": "Delete",
|
||||
"save-button": "Save Changes"
|
||||
},
|
||||
"delete-permission-button": {
|
||||
"label": "Delete",
|
||||
"confirm-alert": {
|
||||
"title": "Delete permission",
|
||||
"message": "Do you really want to delete the permission?",
|
||||
"submit": "Yes",
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"add-permission": {
|
||||
"add-permission-heading": "Add new Permission",
|
||||
"submit-button": "Submit",
|
||||
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"nameHelpText": "The name of the repository. This name will be part of the repository url.",
|
||||
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
|
||||
"contactHelpText": "Email address of the person who is responsible for this repository.",
|
||||
"descriptionHelpText": "A short description of the repository."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,5 +51,14 @@
|
||||
"password-invalid": "Password has to be between 6 and 32 characters",
|
||||
"passwordValidation-invalid": "Passwords have to be the same",
|
||||
"validatePassword": "Please validate password here"
|
||||
},
|
||||
"help": {
|
||||
"usernameHelpText": "Unique name of the user.",
|
||||
"displayNameHelpText": "Display name of the user.",
|
||||
"mailHelpText": "Email address of the user.",
|
||||
"passwordHelpText": "Plain text password of the user.",
|
||||
"passwordConfirmHelpText": "Repeat the password for validation.",
|
||||
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
|
||||
"activeHelpText": "Activate or deactive the user."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ class BaseUrlSettings extends React.Component<Props> {
|
||||
label={t("base-url-settings.force-base-url")}
|
||||
onChange={this.handleForceBaseUrlChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.forceBaseUrlHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("base-url-settings.base-url")}
|
||||
onChange={this.handleBaseUrlChange}
|
||||
value={baseUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.baseUrlHelpText")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,54 +41,63 @@ class GeneralSettings extends React.Component<Props> {
|
||||
onChange={this.handleRealmDescriptionChange}
|
||||
value={realmDescription}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.realmDescriptionHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.date-format")}
|
||||
onChange={this.handleDateFormatChange}
|
||||
value={dateFormat}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.dateFormatHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.plugin-url")}
|
||||
onChange={this.handlePluginUrlChange}
|
||||
value={pluginUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.pluginRepositoryHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.default-namespace-strategy")}
|
||||
onChange={this.handleDefaultNamespaceStrategyChange}
|
||||
value={defaultNamespaceStrategy}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.defaultNameSpaceStrategyHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={enabledXsrfProtection}
|
||||
label={t("general-settings.enabled-xsrf-protection")}
|
||||
onChange={this.handleEnabledXsrfProtectionChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enableXsrfProtectionHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={enableRepositoryArchive}
|
||||
label={t("general-settings.enable-repository-archive")}
|
||||
onChange={this.handleEnableRepositoryArchiveChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enableRepositoryArchiveHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={disableGroupingGrid}
|
||||
label={t("general-settings.disable-grouping-grid")}
|
||||
onChange={this.handleDisableGroupingGridChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.disableGroupingGridHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={anonymousAccessEnabled}
|
||||
label={t("general-settings.anonymous-access-enabled")}
|
||||
onChange={this.handleAnonymousAccessEnabledChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.allowAnonymousAccessHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={skipFailedAuthenticators}
|
||||
label={t("general-settings.skip-failed-authenticators")}
|
||||
onChange={this.handleSkipFailedAuthenticatorsChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.skipFailedAuthenticatorsHelpText")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -47,6 +47,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
disabled={!hasUpdatePermission}
|
||||
validationError={this.state.loginAttemptLimitError}
|
||||
errorMessage={t("validation.login-attempt-limit-invalid")}
|
||||
helpText={t("help.loginAttemptLimitHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("login-attempt.login-attempt-limit-timeout")}
|
||||
@@ -55,6 +56,7 @@ class LoginAttempt extends React.Component<Props, State> {
|
||||
disabled={!hasUpdatePermission}
|
||||
validationError={this.state.loginAttemptLimitTimeoutError}
|
||||
errorMessage={t("validation.login-attempt-limit-timeout-invalid")}
|
||||
helpText={t("help.loginAttemptLimitTimeoutHelpText")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,6 +42,7 @@ class ProxySettings extends React.Component<Props> {
|
||||
label={t("proxy-settings.enable-proxy")}
|
||||
onChange={this.handleEnableProxyChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
helpText={t("help.enableProxyHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-password")}
|
||||
@@ -49,24 +50,28 @@ class ProxySettings extends React.Component<Props> {
|
||||
value={proxyPassword}
|
||||
type="password"
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyPasswordHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-port")}
|
||||
value={proxyPort}
|
||||
onChange={this.handleProxyPortChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyPortHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-server")}
|
||||
value={proxyServer}
|
||||
onChange={this.handleProxyServerChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyServerHelpText")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-user")}
|
||||
value={proxyUser}
|
||||
onChange={this.handleProxyUserChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
helpText={t("help.proxyUserHelpText")}
|
||||
/>
|
||||
<ProxyExcludesTable
|
||||
proxyExcludes={proxyExcludes}
|
||||
|
||||
@@ -24,6 +24,7 @@ class AdminGroupTable extends React.Component<Props, State> {
|
||||
removeLabel={t("admin-settings.remove-group-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
helpText={t("help.adminGroupsHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class AdminUserTable extends React.Component<Props> {
|
||||
removeLabel={t("admin-settings.remove-user-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
helpText={t("help.adminUsersHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
|
||||
import { RemoveEntryOfTableButton, LabelWithHelpIcon } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
items: string[],
|
||||
label: string,
|
||||
removeLabel: string,
|
||||
onRemove: (string[], string) => void,
|
||||
disabled: boolean
|
||||
disabled: boolean,
|
||||
helpText: string
|
||||
};
|
||||
|
||||
class ArrayConfigTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { label, disabled, removeLabel, items } = this.props;
|
||||
const { label, disabled, removeLabel, items, helpText } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<label className="label">{label}</label>
|
||||
<LabelWithHelpIcon label={label} helpText={helpText}/>
|
||||
<table className="table is-hoverable is-fullwidth">
|
||||
<tbody>
|
||||
{items.map(item => {
|
||||
|
||||
@@ -22,6 +22,7 @@ class ProxyExcludesTable extends React.Component<Props, State> {
|
||||
removeLabel={t("proxy-settings.remove-proxy-exclude-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
helpText={t("help.proxyExcludesHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import reducer, {
|
||||
getConfigUpdatePermission
|
||||
} from "./config";
|
||||
|
||||
const CONFIG_URL = "/api/rest/v2/config";
|
||||
const CONFIG_URL = "/api/v2/config";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
@@ -51,8 +51,8 @@ const config = {
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/api/rest/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/rest/v2/config" }
|
||||
self: { href: "http://localhost:8081/api/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/v2/config" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,8 +80,8 @@ const configWithNullValues = {
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/api/rest/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/rest/v2/config" }
|
||||
self: { href: "http://localhost:8081/api/v2/config" },
|
||||
update: { href: "http://localhost:8081/api/v2/config" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should successfully modify config", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should call the callback after modifying config", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail modifying config on HTTP 500", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/config", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import groups from "./groups/modules/groups";
|
||||
import auth from "./modules/auth";
|
||||
import pending from "./modules/pending";
|
||||
import failure from "./modules/failure";
|
||||
import permissions from "./repos/permissions/modules/permissions";
|
||||
import config from "./config/modules/config";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
@@ -26,6 +27,7 @@ function createReduxStore(history: BrowserHistory) {
|
||||
users,
|
||||
repos,
|
||||
repositoryTypes,
|
||||
permissions,
|
||||
groups,
|
||||
auth,
|
||||
config
|
||||
|
||||
@@ -80,6 +80,7 @@ class GroupForm extends React.Component<Props, State> {
|
||||
onChange={this.handleGroupNameChange}
|
||||
value={group.name}
|
||||
validationError={this.state.nameValidationError}
|
||||
helpText={t("group-form.help.nameHelpText")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -93,6 +94,7 @@ class GroupForm extends React.Component<Props, State> {
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={group.description}
|
||||
validationError={false}
|
||||
helpText={t("group-form.help.descriptionHelpText")}
|
||||
/>
|
||||
<MemberNameTable
|
||||
members={this.state.group.members}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
|
||||
import {
|
||||
RemoveEntryOfTableButton,
|
||||
LabelWithHelpIcon
|
||||
} from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
members: string[],
|
||||
@@ -16,7 +19,10 @@ class MemberNameTable extends React.Component<Props, State> {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<label className="label">{t("group.members")}</label>
|
||||
<LabelWithHelpIcon
|
||||
label={t("group.members")}
|
||||
helpText={t("group-form.help.memberHelpText")}
|
||||
/>
|
||||
<table className="table is-hoverable is-fullwidth">
|
||||
<tbody>
|
||||
{this.props.members.map(member => {
|
||||
|
||||
@@ -44,7 +44,7 @@ import reducer, {
|
||||
MODIFY_GROUP_SUCCESS,
|
||||
MODIFY_GROUP_FAILURE
|
||||
} from "./groups";
|
||||
const GROUPS_URL = "/api/rest/v2/groups";
|
||||
const GROUPS_URL = "/api/v2/groups";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
@@ -57,13 +57,13 @@ const humanGroup = {
|
||||
members: ["userZaphod"],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
},
|
||||
delete: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/rest/v2/groups/humanGroup"
|
||||
href:"http://localhost:8081/api/v2/groups/humanGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -72,7 +72,7 @@ const humanGroup = {
|
||||
name: "userZaphod",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/rest/v2/users/userZaphod"
|
||||
href: "http://localhost:8081/api/v2/users/userZaphod"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,13 +89,13 @@ const emptyGroup = {
|
||||
members: [],
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
},
|
||||
delete: {
|
||||
href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/rest/v2/groups/emptyGroup"
|
||||
href:"http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -108,16 +108,16 @@ const responseBody = {
|
||||
pageTotal: 1,
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
|
||||
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
|
||||
},
|
||||
first: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
|
||||
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
|
||||
},
|
||||
last: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
|
||||
href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
|
||||
},
|
||||
create: {
|
||||
href: "http://localhost:3000/api/rest/v2/groups/"
|
||||
href: "http://localhost:3000/api/v2/groups/"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -244,7 +244,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should successfully modify group", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -259,7 +259,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should call the callback after modifying group", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -278,7 +278,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail modifying group on HTTP 500", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
@@ -293,7 +293,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should delete successfully group humanGroup", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should call the callback, after successful delete", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
@@ -324,7 +324,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
it("should fail to delete group humanGroup", () => {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
|
||||
fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch login success and dispatch fetch me", () => {
|
||||
fetchMock.postOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.postOnce("/api/v2/auth/access_token", {
|
||||
body: {
|
||||
cookie: true,
|
||||
grant_type: "password",
|
||||
@@ -88,7 +88,7 @@ describe("auth actions", () => {
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
body: me,
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
@@ -106,7 +106,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch login failure", () => {
|
||||
fetchMock.postOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.postOnce("/api/v2/auth/access_token", {
|
||||
status: 400
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch fetch me success", () => {
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
body: me,
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
@@ -141,7 +141,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch fetch me failure", () => {
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch fetch me unauthorized", () => {
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
status: 401
|
||||
});
|
||||
|
||||
@@ -173,11 +173,11 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch logout success", () => {
|
||||
fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/rest/v2/me", {
|
||||
fetchMock.getOnce("/api/v2/me", {
|
||||
status: 401
|
||||
});
|
||||
|
||||
@@ -194,7 +194,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
it("should dispatch logout failure", () => {
|
||||
fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
|
||||
fetchMock.deleteOnce("/api/v2/auth/access_token", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,20 @@ function extractIdentifierFromFailure(action: Action) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
function removeAllEntriesOfIdentifierFromState(
|
||||
state: Object,
|
||||
payload: any,
|
||||
identifier: string
|
||||
) {
|
||||
const newState = {};
|
||||
for (let failureType in state) {
|
||||
if (failureType !== identifier && !failureType.startsWith(identifier)) {
|
||||
newState[failureType] = state[failureType];
|
||||
}
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
function removeFromState(state: Object, identifier: string) {
|
||||
const newState = {};
|
||||
for (let failureType in state) {
|
||||
@@ -47,7 +61,9 @@ export default function reducer(
|
||||
if (action.itemId) {
|
||||
identifier += "/" + action.itemId;
|
||||
}
|
||||
return removeFromState(state, identifier);
|
||||
if (action.payload)
|
||||
return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
|
||||
else return removeFromState(state, identifier);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
|
||||
@@ -19,6 +19,20 @@ function removeFromState(state: Object, identifier: string) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
function removeAllEntriesOfIdentifierFromState(
|
||||
state: Object,
|
||||
payload: any,
|
||||
identifier: string
|
||||
) {
|
||||
const newState = {};
|
||||
for (let childType in state) {
|
||||
if (childType !== identifier && !childType.startsWith(identifier)) {
|
||||
newState[childType] = state[childType];
|
||||
}
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
function extractIdentifierFromPending(action: Action) {
|
||||
const type = action.type;
|
||||
let identifier = type.substring(0, type.length - PENDING_SUFFIX.length);
|
||||
@@ -48,7 +62,10 @@ export default function reducer(
|
||||
if (action.itemId) {
|
||||
identifier += "/" + action.itemId;
|
||||
}
|
||||
return removeFromState(state, identifier);
|
||||
if (action.payload)
|
||||
return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
|
||||
else
|
||||
return removeFromState(state, identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
scm-ui/src/repos/components/PermissionsNavLink.js
Normal file
28
scm-ui/src/repos/components/PermissionsNavLink.js
Normal file
@@ -0,0 +1,28 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { NavLink } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
permissionUrl: string,
|
||||
t: string => string,
|
||||
repository: Repository
|
||||
};
|
||||
|
||||
class PermissionsNavLink extends React.Component<Props> {
|
||||
hasPermissionsLink = () => {
|
||||
return this.props.repository._links.permissions;
|
||||
};
|
||||
render() {
|
||||
if (!this.hasPermissionsLink()) {
|
||||
return null;
|
||||
}
|
||||
const { permissionUrl, t } = this.props;
|
||||
return (
|
||||
<NavLink to={permissionUrl} label={t("repository-root.permissions")} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(PermissionsNavLink);
|
||||
39
scm-ui/src/repos/components/PermissionsNavLink.test.js
Normal file
39
scm-ui/src/repos/components/PermissionsNavLink.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import "../../tests/enzyme";
|
||||
import "../../tests/i18n";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
import EditNavLink from "./EditNavLink";
|
||||
|
||||
describe("PermissionsNavLink", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
it("should render nothing, if the modify link is missing", () => {
|
||||
const repository = {
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const navLink = shallow(
|
||||
<PermissionsNavLink repository={repository} permissionUrl="" />,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should render the navLink", () => {
|
||||
const repository = {
|
||||
_links: {
|
||||
permissions: {
|
||||
href: "/permissions"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const navLink = mount(
|
||||
<PermissionsNavLink repository={repository} permissionUrl="" />,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("repository-root.permissions");
|
||||
});
|
||||
});
|
||||
@@ -90,12 +90,14 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
value={repository ? repository.contact : ""}
|
||||
validationError={this.state.contactValidationError}
|
||||
errorMessage={t("validation.contact-invalid")}
|
||||
helpText={t("help.contactHelpText")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label={t("repository.description")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={repository ? repository.description : ""}
|
||||
helpText={t("help.descriptionHelpText")}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
@@ -129,12 +131,14 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
value={repository ? repository.name : ""}
|
||||
validationError={this.state.nameValidationError}
|
||||
errorMessage={t("validation.name-invalid")}
|
||||
helpText={t("help.nameHelpText")}
|
||||
/>
|
||||
<Select
|
||||
label={t("repository.type")}
|
||||
onChange={this.handleTypeChange}
|
||||
value={repository ? repository.type : ""}
|
||||
options={this.createSelectOptions(repositoryTypes)}
|
||||
helpText={t("help.typeHelpText")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,9 +22,11 @@ import { translate } from "react-i18next";
|
||||
import RepositoryDetails from "../components/RepositoryDetails";
|
||||
import DeleteNavAction from "../components/DeleteNavAction";
|
||||
import Edit from "../containers/Edit";
|
||||
import Permissions from "../permissions/containers/Permissions";
|
||||
|
||||
import type { History } from "history";
|
||||
import EditNavLink from "../components/EditNavLink";
|
||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||
import ScmDiff from "./ScmDiff";
|
||||
|
||||
type Props = {
|
||||
@@ -112,11 +114,24 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/permissions`}
|
||||
render={props => (
|
||||
<Permissions
|
||||
namespace={this.props.repository.namespace}
|
||||
repoName={this.props.repository.name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
<Section label={t("repository-root.navigation-label")}>
|
||||
<NavLink to={url} label={t("repository-root.information")} />
|
||||
<PermissionsNavLink
|
||||
permissionUrl={`${url}/permissions`}
|
||||
repository={repository}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
</Section>
|
||||
<Section label={t("repository-root.actions-label")}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user