Merge branch 'develop' into feature/import_git_from_url
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Show the date of the last commit for branches in the frontend ([#1439](https://github.com/scm-manager/scm-manager/pull/1439))
|
- Show the date of the last commit for branches in the frontend ([#1439](https://github.com/scm-manager/scm-manager/pull/1439))
|
||||||
- Unify and add description to key view across user settings ([#1440](https://github.com/scm-manager/scm-manager/pull/1440))
|
- Unify and add description to key view across user settings ([#1440](https://github.com/scm-manager/scm-manager/pull/1440))
|
||||||
- Healthcheck for docker image ([#1428](https://github.com/scm-manager/scm-manager/issues/1428) and [#1454](https://github.com/scm-manager/scm-manager/issues/1454))
|
- Healthcheck for docker image ([#1428](https://github.com/scm-manager/scm-manager/issues/1428) and [#1454](https://github.com/scm-manager/scm-manager/issues/1454))
|
||||||
|
- Tags can now be added and deleted through the ui ([#1456](https://github.com/scm-manager/scm-manager/pull/1456))
|
||||||
|
- The ui now displays tag signatures ([#1456](https://github.com/scm-manager/scm-manager/pull/1456))
|
||||||
- Repository import via URL for git ([#1460](https://github.com/scm-manager/scm-manager/pull/1460))
|
- Repository import via URL for git ([#1460](https://github.com/scm-manager/scm-manager/pull/1460))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
|
After Width: | Height: | Size: 127 KiB |
BIN
docs/de/user/repo/assets/repository-code-changeset-with-tag.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 201 KiB |
BIN
docs/de/user/repo/assets/repository-tag-signatures.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
@@ -34,6 +34,19 @@ Beispielsweise wird der Text hitchhiker/HeartOfGold@1a2b3c4 zu einem Link zu dem
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
|
||||||
|
Alle Tags eines Changesets werden in der oberen rechten Ecke der Detailseite angezeigt.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Tags erstellen
|
||||||
|
|
||||||
|
Neue Tags für ein Changeset können direkt in dessen Übersichtsseite erstellt werden.
|
||||||
|
Es muss lediglich ein gewünschter Name angegeben werden, welcher die gleichen Formatierungsbeschränkungen wie Branches erfüllt.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### Datei Details
|
### Datei Details
|
||||||
Nach einem Klick auf eine Datei in den Sources landet man in der Detailansicht der Datei. Dabei sind je nach Dateiformat unterschiedliche Ansichten zu sehen:
|
Nach einem Klick auf eine Datei in den Sources landet man in der Detailansicht der Datei. Dabei sind je nach Dateiformat unterschiedliche Ansichten zu sehen:
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,20 @@ Auf der Tags-Übersicht sind die existierenden Tags nach Erstelldatum absteigend
|
|||||||
Hier wird ein Befehl zum Arbeiten mit dem Tag auf einer Kommandozeile aufgeführt.
|
Hier wird ein Befehl zum Arbeiten mit dem Tag auf einer Kommandozeile aufgeführt.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
#### Tag-Signaturen
|
||||||
|
Wenn mindestens eine Signatur für einen Tag existiert, wird der Verifizierungsstatus des Tags als Schlüsselsymbol hinter dessen Namen in der Detailansicht dargestellt.
|
||||||
|
|
||||||
|
Ein Tag kann mehrere Signaturen haben.
|
||||||
|
|
||||||
|
Abhängig vom Status der einzelnen Signaturen, wird das Symbol entsprechend eingefärbt:
|
||||||
|
- wenn mindestens eine Signatur ungültig ist, wird der Schlüssel `rot` dargestellt ANDERNFALLS
|
||||||
|
- wenn mindestens eine Signatur gültigt ist, wird der Schlüssel `grün` dargestellt ANDERNFALLS
|
||||||
|
- wird der Schlüssel `grau` dargestellt
|
||||||
|
|
||||||
|
Wird der Mauszeiger über das Symbol bewegt, erscheint eine Liste aller Signaturen des Tags.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Tags löschen
|
||||||
|
Tags können direkt von der Übersicht aus oder auf der Detailseite gelöscht werden.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 120 KiB |
BIN
docs/en/user/repo/assets/repository-code-changeset-with-tag.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 201 KiB |
BIN
docs/en/user/repo/assets/repository-tag-signatures.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 211 KiB |
@@ -32,9 +32,20 @@ You can expand the diffs gradually or completely by clicking on the blue bars.
|
|||||||
If commit links formatted like "namespace/name@commitId" are used in the changeset description they will be rendered to internal links.
|
If commit links formatted like "namespace/name@commitId" are used in the changeset description they will be rendered to internal links.
|
||||||
For example the text hitchhiker/HeartOfGold@1a2b3c4 will be transformed to a link directing to the commit 1a2b3c4 of the repository hitchhiker/heartOfGold.
|
For example the text hitchhiker/HeartOfGold@1a2b3c4 will be transformed to a link directing to the commit 1a2b3c4 of the repository hitchhiker/heartOfGold.
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
|
||||||
|
All tags for a changeset are displayed in the top-right corner of the details page.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Creating Tags
|
||||||
|
New tags for a changeset can be created directly on its details page.
|
||||||
|
Only a name has to be provided that meets the same formatting conditions as branches.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### File Details
|
### File Details
|
||||||
After clicking on a file in the sources, the details of the file are shown. Depending on the format of the file, there are different views:
|
After clicking on a file in the sources, the details of the file are shown. Depending on the format of the file, there are different views:
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,20 @@ The tag overview shows the tags that exist for this repository. By clicking on a
|
|||||||
This page shows a command to work with the tag on the command line.
|
This page shows a command to work with the tag on the command line.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
#### Tag Signatures
|
||||||
|
If there is at least one signature on the tag, the verification status is displayed as a key icon after its name on its details page.
|
||||||
|
|
||||||
|
A tag can have multiple signatures.
|
||||||
|
|
||||||
|
Depending on the status of the individual signatures, the key will have a distinct color indicator:
|
||||||
|
- if at least one signature on the tag is invalid, the key will be `red` OTHERWISE
|
||||||
|
- if at least one signature is valid, the key will be `green` OTHERWISE
|
||||||
|
- the key will be `gray`
|
||||||
|
|
||||||
|
If you hover the key icon, a list of all signatures on the tag will pop up.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Deleting Tags
|
||||||
|
Tags can be deleted directly on the tags overview page or on the details page of the tag.
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ import lombok.EqualsAndHashCode;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a tag in a repository.
|
* Represents a tag in a repository.
|
||||||
@@ -41,9 +44,14 @@ import java.util.Optional;
|
|||||||
@Getter
|
@Getter
|
||||||
public final class Tag {
|
public final class Tag {
|
||||||
|
|
||||||
|
public static final String VALID_REV = "[0-9a-z]+";
|
||||||
|
public static final Pattern VALID_REV_PATTERN = Pattern.compile(VALID_REV);
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String revision;
|
private final String revision;
|
||||||
private final Long date;
|
private final Long date;
|
||||||
|
private final List<Signature> signatures = new ArrayList<>();
|
||||||
|
private final Boolean deletable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new tag.
|
* Constructs a new tag.
|
||||||
@@ -65,9 +73,24 @@ public final class Tag {
|
|||||||
* @since 2.5.0
|
* @since 2.5.0
|
||||||
*/
|
*/
|
||||||
public Tag(String name, String revision, Long date) {
|
public Tag(String name, String revision, Long date) {
|
||||||
|
this(name, revision, date, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new tag.
|
||||||
|
*
|
||||||
|
* @param name name of the tag
|
||||||
|
* @param revision tagged revision
|
||||||
|
* @param date the creation timestamp (milliseconds) of the tag
|
||||||
|
* @param deletable whether this tag can be deleted
|
||||||
|
*
|
||||||
|
* @since 2.11.0
|
||||||
|
*/
|
||||||
|
public Tag(String name, String revision, Long date, Boolean deletable) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.revision = revision;
|
this.revision = revision;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
|
this.deletable = deletable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,4 +112,8 @@ public final class Tag {
|
|||||||
public Optional<Long> getDate() {
|
public Optional<Long> getDate() {
|
||||||
return Optional.ofNullable(date);
|
return Optional.ofNullable(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addSignature(Signature signature) {
|
||||||
|
this.signatures.add(signature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,5 +62,10 @@ public enum Command
|
|||||||
/**
|
/**
|
||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
LOOKUP;
|
LOOKUP,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.11.0
|
||||||
|
*/
|
||||||
|
TAG;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,6 +377,18 @@ public final class RepositoryService implements Closeable {
|
|||||||
repository);
|
repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tag command allows the management of repository tags.
|
||||||
|
*
|
||||||
|
* @return instance of {@link TagCommandBuilder}
|
||||||
|
*
|
||||||
|
* @throws CommandNotSupportedException if the command is not supported
|
||||||
|
* by the implementation of the repository service provider.
|
||||||
|
*/
|
||||||
|
public TagCommandBuilder getTagCommand() {
|
||||||
|
return new TagCommandBuilder(provider.getTagCommand());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unbundle command restores a repository from the given bundle.
|
* The unbundle command restores a repository from the given bundle.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.spi.TagCommand;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.11.0
|
||||||
|
*/
|
||||||
|
public final class TagCommandBuilder {
|
||||||
|
private final TagCommand command;
|
||||||
|
|
||||||
|
public TagCommandBuilder(TagCommand command) {
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a command to tag a revision.
|
||||||
|
*
|
||||||
|
* Set parameters and call {@link TagCreateCommandBuilder#execute()}.
|
||||||
|
*
|
||||||
|
* @since 2.11.0
|
||||||
|
*/
|
||||||
|
public TagCreateCommandBuilder create() {
|
||||||
|
return new TagCreateCommandBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a command to delete a tag.
|
||||||
|
*
|
||||||
|
* Set parameters and call {@link TagDeleteCommandBuilder#execute()}.
|
||||||
|
*
|
||||||
|
* @since 2.11.0
|
||||||
|
*/
|
||||||
|
public TagDeleteCommandBuilder delete() {
|
||||||
|
return new TagDeleteCommandBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class TagCreateCommandBuilder {
|
||||||
|
private final TagCreateRequest request = new TagCreateRequest();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param revision The revision identifier for which to create the tag
|
||||||
|
*/
|
||||||
|
public TagCreateCommandBuilder setRevision(String revision) {
|
||||||
|
request.setRevision(revision);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The name of the tag
|
||||||
|
*/
|
||||||
|
public TagCreateCommandBuilder setName(String name) {
|
||||||
|
request.setName(name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tag execute() throws IOException {
|
||||||
|
return command.create(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class TagDeleteCommandBuilder {
|
||||||
|
private final TagDeleteRequest request = new TagDeleteRequest();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The name of the tag that should be deleted
|
||||||
|
*/
|
||||||
|
public TagDeleteCommandBuilder setName(String name) {
|
||||||
|
request.setName(name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute() throws IOException {
|
||||||
|
command.delete(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TagCreateRequest {
|
||||||
|
private String revision;
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TagDeleteRequest {
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -246,6 +246,15 @@ public abstract class RepositoryServiceProvider implements Closeable
|
|||||||
throw new CommandNotSupportedException(Command.TAGS);
|
throw new CommandNotSupportedException(Command.TAGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.11.0
|
||||||
|
*/
|
||||||
|
public TagCommand getTagCommand()
|
||||||
|
{
|
||||||
|
throw new CommandNotSupportedException(Command.TAG);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.api.TagDeleteRequest;
|
||||||
|
import sonia.scm.repository.api.TagCreateRequest;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.11
|
||||||
|
*/
|
||||||
|
public interface TagCommand {
|
||||||
|
Tag create(TagCreateRequest request) throws IOException;
|
||||||
|
void delete(TagDeleteRequest request) throws IOException;
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ public class VndMediaType {
|
|||||||
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
|
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
|
||||||
public static final String TAG = PREFIX + "tag" + SUFFIX;
|
public static final String TAG = PREFIX + "tag" + SUFFIX;
|
||||||
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
|
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
|
||||||
|
public static final String TAG_REQUEST = PREFIX + "tagRequest" + SUFFIX;
|
||||||
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
|
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
|
||||||
public static final String BRANCH_REQUEST = PREFIX + "branchRequest" + SUFFIX;
|
public static final String BRANCH_REQUEST = PREFIX + "branchRequest" + SUFFIX;
|
||||||
public static final String DIFF = PLAIN_TEXT_PREFIX + "diff" + PLAIN_TEXT_SUFFIX;
|
public static final String DIFF = PLAIN_TEXT_PREFIX + "diff" + PLAIN_TEXT_SUFFIX;
|
||||||
|
|||||||
@@ -175,12 +175,12 @@ public class RepositoryAccessITCase {
|
|||||||
.as("assert tag size")
|
.as("assert tag size")
|
||||||
.isNotNull()
|
.isNotNull()
|
||||||
.size()
|
.size()
|
||||||
.isGreaterThan(0);
|
.isPositive();
|
||||||
|
|
||||||
assertThat(response.body().jsonPath().getMap("_embedded.tags.find{it.name=='" + tagName + "'}"))
|
assertThat(response.body().jsonPath().getMap("_embedded.tags.find{it.name=='" + tagName + "'}"))
|
||||||
.as("assert tag has attributes for name, revision, date and links")
|
.as("assert tag has attributes for name, revision, date and links")
|
||||||
.isNotNull()
|
.isNotNull()
|
||||||
.hasSize(4)
|
.hasSize(5)
|
||||||
.containsEntry("name", tagName)
|
.containsEntry("name", tagName)
|
||||||
.containsEntry("revision", changeset.getId());
|
.containsEntry("revision", changeset.getId());
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ import org.eclipse.jgit.util.LfsFactory;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.ContextEntry;
|
import sonia.scm.ContextEntry;
|
||||||
|
import sonia.scm.security.GPG;
|
||||||
|
import sonia.scm.security.PublicKey;
|
||||||
import sonia.scm.util.HttpUtil;
|
import sonia.scm.util.HttpUtil;
|
||||||
import sonia.scm.util.Util;
|
import sonia.scm.util.Util;
|
||||||
import sonia.scm.web.GitUserAgentProvider;
|
import sonia.scm.web.GitUserAgentProvider;
|
||||||
@@ -63,6 +65,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -73,73 +76,37 @@ import static java.util.Optional.of;
|
|||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
public final class GitUtil
|
public final class GitUtil {
|
||||||
{
|
|
||||||
|
|
||||||
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
|
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
public static final String REF_HEAD = "HEAD";
|
public static final String REF_HEAD = "HEAD";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
public static final String REF_HEAD_PREFIX = "refs/heads/";
|
public static final String REF_HEAD_PREFIX = "refs/heads/";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
public static final String REF_MASTER = "master";
|
public static final String REF_MASTER = "master";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String DIRECTORY_DOTGIT = ".git";
|
private static final String DIRECTORY_DOTGIT = ".git";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String DIRECTORY_OBJETCS = "objects";
|
private static final String DIRECTORY_OBJETCS = "objects";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String DIRECTORY_REFS = "refs";
|
private static final String DIRECTORY_REFS = "refs";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String PREFIX_HEADS = "refs/heads/";
|
private static final String PREFIX_HEADS = "refs/heads/";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String PREFIX_TAG = "refs/tags/";
|
private static final String PREFIX_TAG = "refs/tags/";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String REFSPEC = "+refs/heads/*:refs/remote/scm/%s/*";
|
private static final String REFSPEC = "+refs/heads/*:refs/remote/scm/%s/*";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final String REMOTE_REF = "refs/remote/scm/%s/%s";
|
private static final String REMOTE_REF = "refs/remote/scm/%s/%s";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final int TIMEOUT = 5;
|
private static final int TIMEOUT = 5;
|
||||||
|
|
||||||
/** Field description */
|
/**
|
||||||
private static final String USERAGENT_GIT = "git/";
|
* the logger for GitUtil
|
||||||
|
*/
|
||||||
/** the logger for GitUtil */
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
|
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
|
|
||||||
/**
|
private GitUtil() {
|
||||||
* Constructs ...
|
}
|
||||||
*
|
|
||||||
*/
|
|
||||||
private GitUtil() {}
|
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
public static void close(org.eclipse.jgit.lib.Repository repo) {
|
||||||
* Method description
|
if (repo != null) {
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param repo
|
|
||||||
*/
|
|
||||||
public static void close(org.eclipse.jgit.lib.Repository repo)
|
|
||||||
{
|
|
||||||
if (repo != null)
|
|
||||||
{
|
|
||||||
repo.close();
|
repo.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,42 +114,30 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* TODO cache
|
* TODO cache
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param revWalk
|
* @param revWalk
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static Multimap<ObjectId,
|
public static Multimap<ObjectId,
|
||||||
String> createTagMap(org.eclipse.jgit.lib.Repository repository,
|
String> createTagMap(org.eclipse.jgit.lib.Repository repository,
|
||||||
RevWalk revWalk)
|
RevWalk revWalk) {
|
||||||
{
|
|
||||||
Multimap<ObjectId, String> tags = ArrayListMultimap.create();
|
Multimap<ObjectId, String> tags = ArrayListMultimap.create();
|
||||||
|
|
||||||
Map<String, Ref> tagMap = repository.getTags();
|
Map<String, Ref> tagMap = repository.getTags();
|
||||||
|
|
||||||
if (tagMap != null)
|
if (tagMap != null) {
|
||||||
{
|
for (Map.Entry<String, Ref> e : tagMap.entrySet()) {
|
||||||
for (Map.Entry<String, Ref> e : tagMap.entrySet())
|
try {
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
RevCommit c = getCommit(repository, revWalk, e.getValue());
|
RevCommit c = getCommit(repository, revWalk, e.getValue());
|
||||||
|
|
||||||
if (c != null)
|
if (c != null) {
|
||||||
{
|
|
||||||
tags.put(c.getId(), e.getKey());
|
tags.put(c.getId(), e.getKey());
|
||||||
}
|
} else {
|
||||||
else if (logger.isWarnEnabled())
|
|
||||||
{
|
|
||||||
logger.warn("could not find commit for tag {}", e.getKey());
|
logger.warn("could not find commit for tag {}", e.getKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
} catch (IOException ex) {
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
logger.error("could not read commit for ref", ex);
|
logger.error("could not read commit for ref", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +148,7 @@ public final class GitUtil
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static FetchResult fetch(Git git, File directory, Repository remoteRepository) {
|
public static FetchResult fetch(Git git, File directory, Repository remoteRepository) {
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
FetchCommand fetch = git.fetch();
|
FetchCommand fetch = git.fetch();
|
||||||
|
|
||||||
fetch.setRemote(directory.getAbsolutePath());
|
fetch.setRemote(directory.getAbsolutePath());
|
||||||
@@ -202,123 +156,63 @@ public final class GitUtil
|
|||||||
fetch.setTimeout((int) TimeUnit.MINUTES.toSeconds(TIMEOUT));
|
fetch.setTimeout((int) TimeUnit.MINUTES.toSeconds(TIMEOUT));
|
||||||
|
|
||||||
return fetch.call();
|
return fetch.call();
|
||||||
}
|
} catch (GitAPIException ex) {
|
||||||
catch (GitAPIException ex)
|
|
||||||
{
|
|
||||||
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Remote", directory.toString()).in(remoteRepository), "could not fetch", ex);
|
throw new InternalRepositoryException(ContextEntry.ContextBuilder.entity("Remote", directory.toString()).in(remoteRepository), "could not fetch", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param directory
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public static org.eclipse.jgit.lib.Repository open(File directory)
|
public static org.eclipse.jgit.lib.Repository open(File directory)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
|
||||||
FS fs = FS.DETECTED;
|
FS fs = FS.DETECTED;
|
||||||
FileRepositoryBuilder builder = new FileRepositoryBuilder();
|
FileRepositoryBuilder builder = new FileRepositoryBuilder();
|
||||||
|
|
||||||
builder.setFS(fs);
|
builder.setFS(fs);
|
||||||
|
|
||||||
if (isGitDirectory(fs, directory))
|
if (isGitDirectory(fs, directory)) {
|
||||||
{
|
|
||||||
|
|
||||||
// bare repository
|
// bare repository
|
||||||
builder.setGitDir(directory).setBare();
|
builder.setGitDir(directory).setBare();
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.setWorkTree(directory);
|
builder.setWorkTree(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static void release(DiffFormatter formatter) {
|
||||||
* Method description
|
if (formatter != null) {
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param formatter
|
|
||||||
*/
|
|
||||||
public static void release(DiffFormatter formatter)
|
|
||||||
{
|
|
||||||
if (formatter != null)
|
|
||||||
{
|
|
||||||
formatter.close();
|
formatter.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static void release(TreeWalk walk) {
|
||||||
* Method description
|
if (walk != null) {
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param walk
|
|
||||||
*/
|
|
||||||
public static void release(TreeWalk walk)
|
|
||||||
{
|
|
||||||
if (walk != null)
|
|
||||||
{
|
|
||||||
walk.close();
|
walk.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static void release(RevWalk walk) {
|
||||||
* Method description
|
if (walk != null) {
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param walk
|
|
||||||
*/
|
|
||||||
public static void release(RevWalk walk)
|
|
||||||
{
|
|
||||||
if (walk != null)
|
|
||||||
{
|
|
||||||
walk.close();
|
walk.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
/**
|
public static String getBranch(Ref ref) {
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param ref
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getBranch(Ref ref)
|
|
||||||
{
|
|
||||||
String branch = null;
|
String branch = null;
|
||||||
|
|
||||||
if (ref != null)
|
if (ref != null) {
|
||||||
{
|
|
||||||
branch = getBranch(ref.getName());
|
branch = getBranch(ref.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
return branch;
|
return branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static String getBranch(String name) {
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getBranch(String name)
|
|
||||||
{
|
|
||||||
String branch = null;
|
String branch = null;
|
||||||
|
|
||||||
if (Util.isNotEmpty(name) && name.startsWith(PREFIX_HEADS))
|
if (Util.isNotEmpty(name) && name.startsWith(PREFIX_HEADS)) {
|
||||||
{
|
|
||||||
branch = name.substring(PREFIX_HEADS.length());
|
branch = name.substring(PREFIX_HEADS.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,18 +223,15 @@ public final class GitUtil
|
|||||||
* Returns {@code true} if the provided reference name is a branch name.
|
* Returns {@code true} if the provided reference name is a branch name.
|
||||||
*
|
*
|
||||||
* @param refName reference name
|
* @param refName reference name
|
||||||
*
|
|
||||||
* @return {@code true} if the name is a branch name
|
* @return {@code true} if the name is a branch name
|
||||||
*
|
|
||||||
* @since 1.50
|
* @since 1.50
|
||||||
*/
|
*/
|
||||||
public static boolean isBranch(String refName)
|
public static boolean isBranch(String refName) {
|
||||||
{
|
|
||||||
return Strings.nullToEmpty(refName).startsWith(PREFIX_HEADS);
|
return Strings.nullToEmpty(refName).startsWith(PREFIX_HEADS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Ref getBranchIdOrCurrentHead(org.eclipse.jgit.lib.Repository gitRepository, String requestedBranch) throws IOException {
|
public static Ref getBranchIdOrCurrentHead(org.eclipse.jgit.lib.Repository gitRepository, String requestedBranch) throws IOException {
|
||||||
if ( Strings.isNullOrEmpty(requestedBranch) ) {
|
if (Strings.isNullOrEmpty(requestedBranch)) {
|
||||||
logger.trace("no default branch configured, use repository head as default");
|
logger.trace("no default branch configured, use repository head as default");
|
||||||
Optional<Ref> repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository);
|
Optional<Ref> repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository);
|
||||||
return repositoryHeadRef.orElse(null);
|
return repositoryHeadRef.orElse(null);
|
||||||
@@ -352,37 +243,28 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repo
|
* @param repo
|
||||||
* @param branchName
|
* @param branchName
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
|
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
|
||||||
String branchName)
|
String branchName)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
|
||||||
Ref ref = null;
|
Ref ref = null;
|
||||||
if (!branchName.startsWith(REF_HEAD))
|
if (!branchName.startsWith(REF_HEAD)) {
|
||||||
{
|
|
||||||
branchName = PREFIX_HEADS.concat(branchName);
|
branchName = PREFIX_HEADS.concat(branchName);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkBranchName(repo, branchName);
|
checkBranchName(repo, branchName);
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
ref = repo.findRef(branchName);
|
ref = repo.findRef(branchName);
|
||||||
|
|
||||||
if (ref == null)
|
if (ref == null) {
|
||||||
{
|
|
||||||
logger.warn("could not find branch for {}", branchName);
|
logger.warn("could not find branch for {}", branchName);
|
||||||
}
|
}
|
||||||
}
|
} catch (IOException ex) {
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
logger.warn("error occured during resolve of branch id", ex);
|
logger.warn("error occured during resolve of branch id", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,33 +297,21 @@ public final class GitUtil
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Returns the commit for the given ref.
|
||||||
*
|
* If the given ref is for a tag, the commit that this tag belongs to is returned instead.
|
||||||
*
|
|
||||||
* @param repository
|
|
||||||
* @param revWalk
|
|
||||||
* @param ref
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
*/
|
||||||
public static RevCommit getCommit(org.eclipse.jgit.lib.Repository repository,
|
public static RevCommit getCommit(org.eclipse.jgit.lib.Repository repository,
|
||||||
RevWalk revWalk, Ref ref)
|
RevWalk revWalk, Ref ref)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
|
||||||
RevCommit commit = null;
|
RevCommit commit = null;
|
||||||
ObjectId id = ref.getPeeledObjectId();
|
ObjectId id = ref.getPeeledObjectId();
|
||||||
|
|
||||||
if (id == null)
|
if (id == null) {
|
||||||
{
|
|
||||||
id = ref.getObjectId();
|
id = ref.getObjectId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id != null)
|
if (id != null) {
|
||||||
{
|
if (revWalk == null) {
|
||||||
if (revWalk == null)
|
|
||||||
{
|
|
||||||
revWalk = new RevWalk(repository);
|
revWalk = new RevWalk(repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,16 +321,30 @@ public final class GitUtil
|
|||||||
return commit;
|
return commit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RevTag getTag(org.eclipse.jgit.lib.Repository repository,
|
||||||
|
RevWalk revWalk, Ref ref)
|
||||||
|
throws IOException {
|
||||||
|
RevTag tag = null;
|
||||||
|
ObjectId id = ref.getObjectId();
|
||||||
|
|
||||||
|
if (id != null) {
|
||||||
|
if (revWalk == null) {
|
||||||
|
revWalk = new RevWalk(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = revWalk.parseTag(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param commit
|
* @param commit
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static long getCommitTime(RevCommit commit)
|
public static long getCommitTime(RevCommit commit) {
|
||||||
{
|
|
||||||
long date = commit.getCommitTime();
|
long date = commit.getCommitTime();
|
||||||
|
|
||||||
date = date * 1000;
|
date = date * 1000;
|
||||||
@@ -471,17 +355,13 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param objectId
|
* @param objectId
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String getId(AnyObjectId objectId)
|
public static String getId(AnyObjectId objectId) {
|
||||||
{
|
|
||||||
String id = Util.EMPTY_STRING;
|
String id = Util.EMPTY_STRING;
|
||||||
|
|
||||||
if (objectId != null)
|
if (objectId != null) {
|
||||||
{
|
|
||||||
id = objectId.name();
|
id = objectId.name();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,44 +371,27 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param id
|
* @param id
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public static Ref getRefForCommit(org.eclipse.jgit.lib.Repository repository,
|
public static Ref getRefForCommit(org.eclipse.jgit.lib.Repository repository,
|
||||||
ObjectId id)
|
ObjectId id)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
|
||||||
Ref ref = null;
|
Ref ref = null;
|
||||||
RevWalk walk = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
walk = new RevWalk(repository);
|
|
||||||
|
|
||||||
|
try (RevWalk walk = new RevWalk(repository)) {
|
||||||
RevCommit commit = walk.parseCommit(id);
|
RevCommit commit = walk.parseCommit(id);
|
||||||
|
|
||||||
for (Map.Entry<String, Ref> e : repository.getAllRefs().entrySet())
|
for (Map.Entry<String, Ref> e : repository.getAllRefs().entrySet()) {
|
||||||
{
|
if (e.getKey().startsWith(Constants.R_HEADS) && walk.isMergedInto(commit,
|
||||||
if (e.getKey().startsWith(Constants.R_HEADS))
|
walk.parseCommit(e.getValue().getObjectId()))) {
|
||||||
{
|
ref = e.getValue();
|
||||||
if (walk.isMergedInto(commit,
|
|
||||||
walk.parseCommit(e.getValue().getObjectId())))
|
|
||||||
{
|
|
||||||
ref = e.getValue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
release(walk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
@@ -580,26 +443,19 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repo
|
* @param repo
|
||||||
* @param revision
|
* @param revision
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public static ObjectId getRevisionId(org.eclipse.jgit.lib.Repository repo,
|
public static ObjectId getRevisionId(org.eclipse.jgit.lib.Repository repo,
|
||||||
String revision)
|
String revision)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
|
||||||
ObjectId revId;
|
ObjectId revId;
|
||||||
|
|
||||||
if (Util.isNotEmpty(revision))
|
if (Util.isNotEmpty(revision)) {
|
||||||
{
|
|
||||||
revId = repo.resolve(revision);
|
revId = repo.resolve(revision);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
revId = getRepositoryHead(repo);
|
revId = getRepositoryHead(repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,34 +465,27 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param localBranch
|
* @param localBranch
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String getScmRemoteRefName(Repository repository,
|
public static String getScmRemoteRefName(Repository repository,
|
||||||
Ref localBranch)
|
Ref localBranch) {
|
||||||
{
|
|
||||||
return getScmRemoteRefName(repository, localBranch.getName());
|
return getScmRemoteRefName(repository, localBranch.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param localBranch
|
* @param localBranch
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String getScmRemoteRefName(Repository repository,
|
public static String getScmRemoteRefName(Repository repository,
|
||||||
String localBranch)
|
String localBranch) {
|
||||||
{
|
|
||||||
String branch = localBranch;
|
String branch = localBranch;
|
||||||
|
|
||||||
if (localBranch.startsWith(REF_HEAD_PREFIX))
|
if (localBranch.startsWith(REF_HEAD_PREFIX)) {
|
||||||
{
|
|
||||||
branch = localBranch.substring(REF_HEAD_PREFIX.length());
|
branch = localBranch.substring(REF_HEAD_PREFIX.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,16 +496,12 @@ public final class GitUtil
|
|||||||
* Returns the name of the tag or {@code null} if the the ref is not a tag.
|
* Returns the name of the tag or {@code null} if the the ref is not a tag.
|
||||||
*
|
*
|
||||||
* @param refName ref name
|
* @param refName ref name
|
||||||
*
|
|
||||||
* @return name of tag or {@link null}
|
* @return name of tag or {@link null}
|
||||||
*
|
|
||||||
* @since 1.50
|
* @since 1.50
|
||||||
*/
|
*/
|
||||||
public static String getTagName(String refName)
|
public static String getTagName(String refName) {
|
||||||
{
|
|
||||||
String tagName = null;
|
String tagName = null;
|
||||||
if (refName.startsWith(PREFIX_TAG))
|
if (refName.startsWith(PREFIX_TAG)) {
|
||||||
{
|
|
||||||
tagName = refName.substring(PREFIX_TAG.length());
|
tagName = refName.substring(PREFIX_TAG.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,91 +511,112 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param ref
|
* @param ref
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static String getTagName(Ref ref)
|
public static String getTagName(Ref ref) {
|
||||||
{
|
|
||||||
String name = ref.getName();
|
String name = ref.getName();
|
||||||
|
|
||||||
if (name.startsWith(PREFIX_TAG))
|
if (name.startsWith(PREFIX_TAG)) {
|
||||||
{
|
|
||||||
name = name.substring(PREFIX_TAG.length());
|
name = name.substring(PREFIX_TAG.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String GPG_HEADER = "-----BEGIN PGP SIGNATURE-----";
|
||||||
|
|
||||||
|
public static Optional<Signature> getTagSignature(RevObject revObject, GPG gpg, RevWalk revWalk) throws IOException {
|
||||||
|
if (revObject instanceof RevTag) {
|
||||||
|
final byte[] messageBytes = revWalk.getObjectReader().open(revObject.getId()).getBytes();
|
||||||
|
final String message = new String(messageBytes);
|
||||||
|
final int signatureStartIndex = message.indexOf(GPG_HEADER);
|
||||||
|
if (signatureStartIndex < 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String signature = message.substring(signatureStartIndex);
|
||||||
|
|
||||||
|
String publicKeyId = gpg.findPublicKeyId(signature.getBytes());
|
||||||
|
if (Strings.isNullOrEmpty(publicKeyId)) {
|
||||||
|
// key not found
|
||||||
|
return Optional.of(new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<PublicKey> publicKeyById = gpg.findPublicKey(publicKeyId);
|
||||||
|
if (!publicKeyById.isPresent()) {
|
||||||
|
// key not found
|
||||||
|
return Optional.of(new Signature(publicKeyId, "gpg", SignatureStatus.NOT_FOUND, null, Collections.emptySet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
PublicKey publicKey = publicKeyById.get();
|
||||||
|
|
||||||
|
String rawMessage = message.substring(0, signatureStartIndex);
|
||||||
|
boolean verified = publicKey.verify(rawMessage.getBytes(), signature.getBytes());
|
||||||
|
return Optional.of(new Signature(
|
||||||
|
publicKeyId,
|
||||||
|
"gpg",
|
||||||
|
verified ? SignatureStatus.VERIFIED : SignatureStatus.INVALID,
|
||||||
|
publicKey.getOwner().orElse(null),
|
||||||
|
publicKey.getContacts()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the request comes from a git client.
|
* Returns true if the request comes from a git client.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param request servlet request
|
* @param request servlet request
|
||||||
*
|
|
||||||
* @return true if the client is git
|
* @return true if the client is git
|
||||||
*/
|
*/
|
||||||
public static boolean isGitClient(HttpServletRequest request)
|
public static boolean isGitClient(HttpServletRequest request) {
|
||||||
{
|
|
||||||
return GIT_USER_AGENT_PROVIDER.parseUserAgent(request.getHeader(HttpUtil.HEADER_USERAGENT)) != null;
|
return GIT_USER_AGENT_PROVIDER.parseUserAgent(request.getHeader(HttpUtil.HEADER_USERAGENT)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param dir
|
* @param dir
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static boolean isGitDirectory(File dir)
|
public static boolean isGitDirectory(File dir) {
|
||||||
{
|
|
||||||
return isGitDirectory(FS.DETECTED, dir);
|
return isGitDirectory(FS.DETECTED, dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param fs
|
* @param fs
|
||||||
* @param dir
|
* @param dir
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static boolean isGitDirectory(FS fs, File dir)
|
public static boolean isGitDirectory(FS fs, File dir) {
|
||||||
{
|
|
||||||
//J-
|
//J-
|
||||||
return fs.resolve(dir, DIRECTORY_OBJETCS).exists()
|
return fs.resolve(dir, DIRECTORY_OBJETCS).exists()
|
||||||
&& fs.resolve(dir, DIRECTORY_REFS).exists()
|
&& fs.resolve(dir, DIRECTORY_REFS).exists()
|
||||||
&&!fs.resolve(dir, DIRECTORY_DOTGIT).exists();
|
&& !fs.resolve(dir, DIRECTORY_DOTGIT).exists();
|
||||||
//J+
|
//J+
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param ref
|
* @param ref
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static boolean isHead(String ref)
|
public static boolean isHead(String ref) {
|
||||||
{
|
|
||||||
return ref.startsWith(REF_HEAD_PREFIX);
|
return ref.startsWith(REF_HEAD_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param id
|
* @param id
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static boolean isValidObjectId(ObjectId id)
|
public static boolean isValidObjectId(ObjectId id) {
|
||||||
{
|
return (id != null) && !id.equals(ObjectId.zeroId());
|
||||||
return (id != null) &&!id.equals(ObjectId.zeroId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -792,25 +658,20 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repo
|
* @param repo
|
||||||
* @param branchName
|
* @param branchName
|
||||||
*
|
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static void checkBranchName(org.eclipse.jgit.lib.Repository repo,
|
static void checkBranchName(org.eclipse.jgit.lib.Repository repo,
|
||||||
String branchName)
|
String branchName)
|
||||||
throws IOException
|
throws IOException {
|
||||||
{
|
if (branchName.contains("..")) {
|
||||||
if (branchName.contains(".."))
|
|
||||||
{
|
|
||||||
File repoDirectory = repo.getDirectory();
|
File repoDirectory = repo.getDirectory();
|
||||||
File branchFile = new File(repoDirectory, branchName);
|
File branchFile = new File(repoDirectory, branchName);
|
||||||
|
|
||||||
if (!branchFile.getCanonicalPath().startsWith(
|
if (!branchFile.getCanonicalPath().startsWith(
|
||||||
repoDirectory.getCanonicalPath()))
|
repoDirectory.getCanonicalPath())) {
|
||||||
{
|
|
||||||
logger.error(
|
logger.error(
|
||||||
"branch \"{}\" is outside of the repository. It looks like path traversal attack",
|
"branch \"{}\" is outside of the repository. It looks like path traversal attack",
|
||||||
branchName);
|
branchName);
|
||||||
@@ -824,13 +685,10 @@ public final class GitUtil
|
|||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @param repository
|
* @param repository
|
||||||
*
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private static RefSpec createRefSpec(Repository repository)
|
private static RefSpec createRefSpec(Repository repository) {
|
||||||
{
|
|
||||||
return new RefSpec(String.format(REFSPEC, repository.getId()));
|
return new RefSpec(String.format(REFSPEC, repository.getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
Command.DIFF,
|
Command.DIFF,
|
||||||
Command.DIFF_RESULT,
|
Command.DIFF_RESULT,
|
||||||
Command.LOG,
|
Command.LOG,
|
||||||
|
Command.TAG,
|
||||||
Command.TAGS,
|
Command.TAGS,
|
||||||
Command.BRANCH,
|
Command.BRANCH,
|
||||||
Command.BRANCHES,
|
Command.BRANCHES,
|
||||||
@@ -142,7 +143,12 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TagsCommand getTagsCommand() {
|
public TagsCommand getTagsCommand() {
|
||||||
return new GitTagsCommand(context);
|
return commandInjector.getInstance(GitTagsCommand.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TagCommand getTagCommand() {
|
||||||
|
return commandInjector.getInstance(GitTagCommand.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.eclipse.jgit.revwalk.RevObject;
|
||||||
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.repository.GitUtil;
|
||||||
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
|
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.RepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.RepositoryHookType;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.api.HookContext;
|
||||||
|
import sonia.scm.repository.api.HookContextFactory;
|
||||||
|
import sonia.scm.repository.api.HookFeature;
|
||||||
|
import sonia.scm.repository.api.HookTagProvider;
|
||||||
|
import sonia.scm.repository.api.TagCreateRequest;
|
||||||
|
import sonia.scm.repository.api.TagDeleteRequest;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.singleton;
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
|
public class GitTagCommand extends AbstractGitCommand implements TagCommand {
|
||||||
|
private final HookContextFactory hookContextFactory;
|
||||||
|
private final ScmEventBus eventBus;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
GitTagCommand(GitContext context, HookContextFactory hookContextFactory, ScmEventBus eventBus) {
|
||||||
|
super(context);
|
||||||
|
this.hookContextFactory = hookContextFactory;
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tag create(TagCreateRequest request) {
|
||||||
|
final String name = request.getName();
|
||||||
|
final String revision = request.getRevision();
|
||||||
|
|
||||||
|
if (Strings.isNullOrEmpty(revision)) {
|
||||||
|
throw new IllegalArgumentException("Revision is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Strings.isNullOrEmpty(name)) {
|
||||||
|
throw new IllegalArgumentException("Name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Git git = new Git(context.open())) {
|
||||||
|
|
||||||
|
RevObject revObject;
|
||||||
|
Long tagTime;
|
||||||
|
|
||||||
|
ObjectId taggedCommitObjectId = git.getRepository().resolve(revision);
|
||||||
|
|
||||||
|
if (taggedCommitObjectId == null) {
|
||||||
|
throw notFound(entity("revision", revision).in(repository));
|
||||||
|
}
|
||||||
|
|
||||||
|
try (RevWalk walk = new RevWalk(git.getRepository())) {
|
||||||
|
revObject = walk.parseAny(taggedCommitObjectId);
|
||||||
|
tagTime = GitUtil.getTagTime(walk, taggedCommitObjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag tag = new Tag(name, revision, tagTime);
|
||||||
|
|
||||||
|
RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.createHookEvent(tag));
|
||||||
|
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
|
||||||
|
|
||||||
|
User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
|
||||||
|
PersonIdent taggerIdent = new PersonIdent(user.getDisplayName(), user.getMail());
|
||||||
|
|
||||||
|
git.tag()
|
||||||
|
.setObjectId(revObject)
|
||||||
|
.setTagger(taggerIdent)
|
||||||
|
.setName(name)
|
||||||
|
.call();
|
||||||
|
|
||||||
|
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
} catch (IOException | GitAPIException ex) {
|
||||||
|
throw new InternalRepositoryException(repository, "could not create tag " + name + " for revision " + revision, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(TagDeleteRequest request) {
|
||||||
|
String name = request.getName();
|
||||||
|
|
||||||
|
if (Strings.isNullOrEmpty(name)) {
|
||||||
|
throw new IllegalArgumentException("Name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Git git = new Git(context.open())) {
|
||||||
|
final Repository repository = git.getRepository();
|
||||||
|
Optional<Ref> tagRef = findTagRef(git, name);
|
||||||
|
Tag tag;
|
||||||
|
|
||||||
|
// Deleting a non-existent tag is a valid action and simply succeeds without
|
||||||
|
// anything happening.
|
||||||
|
if (!tagRef.isPresent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (RevWalk walk = new RevWalk(repository)) {
|
||||||
|
final RevCommit commit = GitUtil.getCommit(repository, walk, tagRef.get());
|
||||||
|
Long tagTime = GitUtil.getTagTime(walk, tagRef.get().getObjectId());
|
||||||
|
tag = new Tag(name, commit.name(), tagTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
RepositoryHookEvent hookEvent = createTagHookEvent(TagHookContextProvider.deleteHookEvent(tag));
|
||||||
|
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
|
||||||
|
git.tagDelete().setTags(name).call();
|
||||||
|
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
|
||||||
|
} catch (GitAPIException | IOException e) {
|
||||||
|
throw new InternalRepositoryException(repository, "could not delete tag " + name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Ref> findTagRef(Git git, String name) throws GitAPIException {
|
||||||
|
final String tagRef = "refs/tags/" + name;
|
||||||
|
return git.tagList().call().stream().filter(it -> it.getName().equals(tagRef)).findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepositoryHookEvent createTagHookEvent(TagHookContextProvider hookEvent) {
|
||||||
|
HookContext context = hookContextFactory.createContext(hookEvent, this.context.getRepository());
|
||||||
|
return new RepositoryHookEvent(context, this.context.getRepository(), RepositoryHookType.PRE_RECEIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TagHookContextProvider extends HookContextProvider {
|
||||||
|
private final List<Tag> newTags;
|
||||||
|
private final List<Tag> deletedTags;
|
||||||
|
|
||||||
|
private TagHookContextProvider(List<Tag> newTags, List<Tag> deletedTags) {
|
||||||
|
this.newTags = newTags;
|
||||||
|
this.deletedTags = deletedTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TagHookContextProvider createHookEvent(Tag newTag) {
|
||||||
|
return new TagHookContextProvider(singletonList(newTag), emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
static TagHookContextProvider deleteHookEvent(Tag deletedTag) {
|
||||||
|
return new TagHookContextProvider(emptyList(), singletonList(deletedTag));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<HookFeature> getSupportedFeatures() {
|
||||||
|
return singleton(HookFeature.TAG_PROVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HookTagProvider getTagProvider() {
|
||||||
|
return new HookTagProvider() {
|
||||||
|
@Override
|
||||||
|
public List<Tag> getCreatedTags() {
|
||||||
|
return newTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Tag> getDeletedTags() {
|
||||||
|
return deletedTags;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HookChangesetProvider getChangesetProvider() {
|
||||||
|
return r -> new HookChangesetResponse(emptyList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,15 +30,21 @@ import com.google.common.base.Function;
|
|||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import org.eclipse.jgit.api.Git;
|
import org.eclipse.jgit.api.Git;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.Constants;
|
||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
import org.eclipse.jgit.revwalk.RevObject;
|
import org.eclipse.jgit.revwalk.RevObject;
|
||||||
|
import org.eclipse.jgit.revwalk.RevTag;
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
import org.eclipse.jgit.revwalk.RevWalk;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.repository.GitUtil;
|
import sonia.scm.repository.GitUtil;
|
||||||
import sonia.scm.repository.InternalRepositoryException;
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.security.GPG;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -49,32 +55,34 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
||||||
|
|
||||||
|
private final GPG gpg;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs ...
|
* Constructs ...
|
||||||
*
|
*
|
||||||
* @param context
|
* @param context
|
||||||
*/
|
*/
|
||||||
public GitTagsCommand(GitContext context) {
|
@Inject
|
||||||
|
public GitTagsCommand(GitContext context, GPG gpp) {
|
||||||
super(context);
|
super(context);
|
||||||
|
this.gpg = gpp;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Tag> getTags() throws IOException {
|
public List<Tag> getTags() throws IOException {
|
||||||
List<Tag> tags = null;
|
List<Tag> tags;
|
||||||
|
|
||||||
RevWalk revWalk = null;
|
RevWalk revWalk = null;
|
||||||
|
|
||||||
try {
|
try (Git git = new Git(open())) {
|
||||||
final Git git = new Git(open());
|
|
||||||
|
|
||||||
revWalk = new RevWalk(git.getRepository());
|
revWalk = new RevWalk(git.getRepository());
|
||||||
|
|
||||||
List<Ref> tagList = git.tagList().call();
|
List<Ref> tagList = git.tagList().call();
|
||||||
|
|
||||||
tags = Lists.transform(tagList,
|
tags = Lists.transform(tagList,
|
||||||
new TransformFuntion(git.getRepository(), revWalk));
|
new TransformFunction(git.getRepository(), revWalk, gpg));
|
||||||
} catch (GitAPIException ex) {
|
} catch (GitAPIException ex) {
|
||||||
throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
|
throw new InternalRepositoryException(repository, "could not read tags from repository", ex);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -92,26 +100,27 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
|||||||
* @author Enter your name here...
|
* @author Enter your name here...
|
||||||
* @version Enter version here..., 12/07/06
|
* @version Enter version here..., 12/07/06
|
||||||
*/
|
*/
|
||||||
private static class TransformFuntion implements Function<Ref, Tag> {
|
private static class TransformFunction implements Function<Ref, Tag> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the logger for TransformFuntion
|
* the logger for TransformFuntion
|
||||||
*/
|
*/
|
||||||
private static final Logger logger =
|
private static final Logger logger =
|
||||||
LoggerFactory.getLogger(TransformFuntion.class);
|
LoggerFactory.getLogger(TransformFunction.class);
|
||||||
|
|
||||||
//~--- constructors -------------------------------------------------------
|
//~--- constructors -------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs ...
|
* Constructs ...
|
||||||
*
|
* @param repository
|
||||||
* @param repository
|
|
||||||
* @param revWalk
|
* @param revWalk
|
||||||
*/
|
*/
|
||||||
public TransformFuntion(org.eclipse.jgit.lib.Repository repository,
|
public TransformFunction(Repository repository,
|
||||||
RevWalk revWalk) {
|
RevWalk revWalk,
|
||||||
|
GPG gpg) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.revWalk = revWalk;
|
this.revWalk = revWalk;
|
||||||
|
this.gpg = gpg;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods ------------------------------------------------------------
|
//~--- methods ------------------------------------------------------------
|
||||||
@@ -127,14 +136,17 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
|||||||
Tag tag = null;
|
Tag tag = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RevObject revObject = GitUtil.getCommit(repository, revWalk, ref);
|
RevCommit revCommit = GitUtil.getCommit(repository, revWalk, ref);
|
||||||
|
if (revCommit != null) {
|
||||||
if (revObject != null) {
|
|
||||||
String name = GitUtil.getTagName(ref);
|
String name = GitUtil.getTagName(ref);
|
||||||
|
tag = new Tag(name, revCommit.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId()));
|
||||||
tag = new Tag(name, revObject.getId().name(), GitUtil.getTagTime(revWalk, ref.getObjectId()));
|
RevObject revObject = revWalk.parseAny(ref.getObjectId());
|
||||||
|
if (revObject.getType() == Constants.OBJ_TAG) {
|
||||||
|
RevTag revTag = (RevTag) revObject;
|
||||||
|
GitUtil.getTagSignature(revTag, gpg, revWalk)
|
||||||
|
.ifPresent(tag::addSignature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
logger.error("could not get commit for tag", ex);
|
logger.error("could not get commit for tag", ex);
|
||||||
}
|
}
|
||||||
@@ -147,11 +159,12 @@ public class GitTagsCommand extends AbstractGitCommand implements TagsCommand {
|
|||||||
/**
|
/**
|
||||||
* Field description
|
* Field description
|
||||||
*/
|
*/
|
||||||
private org.eclipse.jgit.lib.Repository repository;
|
private final org.eclipse.jgit.lib.Repository repository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Field description
|
* Field description
|
||||||
*/
|
*/
|
||||||
private RevWalk revWalk;
|
private final RevWalk revWalk;
|
||||||
|
private final GPG gpg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import org.mockito.invocation.InvocationOnMock;
|
|||||||
import org.mockito.junit.MockitoJUnitRunner;
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.repository.Branch;
|
import sonia.scm.repository.Branch;
|
||||||
import sonia.scm.repository.BranchCreatedEvent;
|
|
||||||
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
|
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
|
||||||
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
|
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
|
||||||
import sonia.scm.repository.api.BranchRequest;
|
import sonia.scm.repository.api.BranchRequest;
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.eclipse.jgit.lib.GpgSigner;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.repository.GitTestHelper;
|
||||||
|
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.api.HookContext;
|
||||||
|
import sonia.scm.repository.api.HookContextFactory;
|
||||||
|
import sonia.scm.repository.api.TagDeleteRequest;
|
||||||
|
import sonia.scm.repository.api.TagCreateRequest;
|
||||||
|
import sonia.scm.security.GPG;
|
||||||
|
import sonia.scm.util.MockUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class GitTagCommandTest extends AbstractGitCommandTestBase {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GPG gpg;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HookContextFactory hookContextFactory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ScmEventBus eventBus;
|
||||||
|
|
||||||
|
private Subject subject;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setSigner() {
|
||||||
|
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void bindThreadContext() {
|
||||||
|
SecurityUtils.setSecurityManager(new DefaultSecurityManager());
|
||||||
|
subject = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void unbindThreadContext() {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
ThreadContext.unbindSecurityManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateATag() throws IOException {
|
||||||
|
createCommand().create(new TagCreateRequest("592d797cd36432e591416e8b2b98154f4f163411", "newtag"));
|
||||||
|
Optional<Tag> optionalTag = findTag(createContext(), "newtag");
|
||||||
|
assertThat(optionalTag).isNotEmpty();
|
||||||
|
final Tag tag = optionalTag.get();
|
||||||
|
assertThat(tag.getName()).isEqualTo("newtag");
|
||||||
|
assertThat(tag.getRevision()).isEqualTo("592d797cd36432e591416e8b2b98154f4f163411");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldPostCreateEvent() {
|
||||||
|
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
|
||||||
|
doNothing().when(eventBus).post(captor.capture());
|
||||||
|
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
|
||||||
|
|
||||||
|
createCommand().create(new TagCreateRequest("592d797cd36432e591416e8b2b98154f4f163411", "newtag"));
|
||||||
|
|
||||||
|
List<Object> events = captor.getAllValues();
|
||||||
|
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
|
||||||
|
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
|
||||||
|
|
||||||
|
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
|
||||||
|
assertThat(event.getContext().getTagProvider().getCreatedTags().get(0).getName()).isEqualTo("newtag");
|
||||||
|
assertThat(event.getContext().getTagProvider().getDeletedTags()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldDeleteATag() throws IOException {
|
||||||
|
final GitContext context = createContext();
|
||||||
|
Optional<Tag> tag = findTag(context, "test-tag");
|
||||||
|
assertThat(tag).isNotEmpty();
|
||||||
|
|
||||||
|
createCommand().delete(new TagDeleteRequest("test-tag"));
|
||||||
|
|
||||||
|
tag = findTag(context, "test-tag");
|
||||||
|
assertThat(tag).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldPostDeleteEvent() {
|
||||||
|
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
|
||||||
|
doNothing().when(eventBus).post(captor.capture());
|
||||||
|
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
|
||||||
|
|
||||||
|
createCommand().delete(new TagDeleteRequest("test-tag"));
|
||||||
|
|
||||||
|
List<Object> events = captor.getAllValues();
|
||||||
|
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
|
||||||
|
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
|
||||||
|
|
||||||
|
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
|
||||||
|
assertThat(event.getContext().getTagProvider().getCreatedTags()).isEmpty();
|
||||||
|
final Tag deletedTag = event.getContext().getTagProvider().getDeletedTags().get(0);
|
||||||
|
assertThat(deletedTag.getName()).isEqualTo("test-tag");
|
||||||
|
assertThat(deletedTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private GitTagCommand createCommand() {
|
||||||
|
return new GitTagCommand(createContext(), hookContextFactory, eventBus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Tag> readTags(GitContext context) throws IOException {
|
||||||
|
return new GitTagsCommand(context, gpg).getTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Tag> findTag(GitContext context, String name) throws IOException {
|
||||||
|
List<Tag> tags = readTags(context);
|
||||||
|
return tags.stream().filter(t -> name.equals(t.getName())).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HookContext createMockedContext(InvocationOnMock invocation) {
|
||||||
|
HookContext mock = mock(HookContext.class);
|
||||||
|
when(mock.getTagProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getTagProvider());
|
||||||
|
return mock;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,48 +24,86 @@
|
|||||||
|
|
||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
import com.github.sdorra.shiro.ShiroRule;
|
|
||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import sonia.scm.repository.SignatureStatus;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.security.GPG;
|
||||||
|
import sonia.scm.security.PublicKey;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
|
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
public class GitTagsCommandTest extends AbstractGitCommandTestBase {
|
public class GitTagsCommandTest extends AbstractGitCommandTestBase {
|
||||||
|
|
||||||
@Rule
|
@Mock
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
GPG gpg;
|
||||||
|
|
||||||
@Rule
|
@Mock
|
||||||
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
|
PublicKey publicKey;
|
||||||
|
|
||||||
@Rule
|
|
||||||
public ShiroRule shiro = new ShiroRule();
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetDatesCorrectly() throws IOException {
|
public void shouldGetDatesCorrectly() throws IOException {
|
||||||
final GitContext gitContext = createContext();
|
final GitContext gitContext = createContext();
|
||||||
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext);
|
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
|
||||||
final List<Tag> tags = tagsCommand.getTags();
|
final List<Tag> tags = tagsCommand.getTags();
|
||||||
assertThat(tags).hasSize(2);
|
assertThat(tags).hasSize(3);
|
||||||
|
|
||||||
Tag annotatedTag = tags.get(0);
|
Tag annotatedTag = tags.get(0);
|
||||||
assertThat(annotatedTag.getName()).isEqualTo("1.0.0");
|
assertThat(annotatedTag.getName()).isEqualTo("1.0.0");
|
||||||
assertThat(annotatedTag.getDate()).contains(1598348105000L); // Annotated - Take tag date
|
assertThat(annotatedTag.getDate()).contains(1598348105000L); // Annotated - Take tag date
|
||||||
assertThat(annotatedTag.getRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
|
assertThat(annotatedTag.getRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
|
||||||
|
|
||||||
Tag lightweightTag = tags.get(1);
|
Tag lightweightTag = tags.get(2);
|
||||||
assertThat(lightweightTag.getName()).isEqualTo("test-tag");
|
assertThat(lightweightTag.getName()).isEqualTo("test-tag");
|
||||||
assertThat(lightweightTag.getDate()).contains(1339416344000L); // Lightweight - Take commit date
|
assertThat(lightweightTag.getDate()).contains(1339416344000L); // Lightweight - Take commit date
|
||||||
assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
assertThat(lightweightTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGetSignatures() throws IOException {
|
||||||
|
when(gpg.findPublicKeyId(ArgumentMatchers.any())).thenReturn("2BA27721F113C005CC16F06BAE63EFBC49F140CF");
|
||||||
|
when(gpg.findPublicKey("2BA27721F113C005CC16F06BAE63EFBC49F140CF")).thenReturn(Optional.of(publicKey));
|
||||||
|
String signature = "-----BEGIN PGP SIGNATURE-----\n" +
|
||||||
|
"\n" +
|
||||||
|
"iQEzBAABCgAdFiEEK6J3IfETwAXMFvBrrmPvvEnxQM8FAl+9acoACgkQrmPvvEnx\n" +
|
||||||
|
"QM9abwgAnGP+Y/Ijli+PAsimfOmZQWYepjptoOv9m7i3bnHv8V+Qg6cm51I3E0YV\n" +
|
||||||
|
"R2QaxxzW9PgS4hcES+L1qs8Lwo18RurF469eZEmNb8DcUFJ3sEWeHlIl5wZNNo/v\n" +
|
||||||
|
"jJm0d9LNcSmtAIiQ8eDMoGdFXJzHewGickLOSsQGmfZgZus4Qlsh7r3BZTI1Zwd/\n" +
|
||||||
|
"6jaBFctX13FuepCTxq2SjEfRaQHIYkyFQq2o6mjL5S2qfYJ/S//gcCCzxllQrisF\n" +
|
||||||
|
"5fRW3LzLI4eXFH0vua7+UzNS2Rwpifg2OENJA/Kn+3R36LWEGxFK9pNqjVPRAcQj\n" +
|
||||||
|
"1vSkcjK26RqhAqCjNLSagM8ATZrh+g==\n" +
|
||||||
|
"=kUKm\n" +
|
||||||
|
"-----END PGP SIGNATURE-----\n";
|
||||||
|
String signedContent = "object 592d797cd36432e591416e8b2b98154f4f163411\n" +
|
||||||
|
"type commit\n" +
|
||||||
|
"tag signedtag\n" +
|
||||||
|
"tagger Arthur Dent <arthur.dent@hitchhiker.com> 1606248906 +0100\n" +
|
||||||
|
"\n" +
|
||||||
|
"this tag is signed\n";
|
||||||
|
when(publicKey.verify(signedContent.getBytes(), signature.getBytes())).thenReturn(true);
|
||||||
|
|
||||||
|
final GitContext gitContext = createContext();
|
||||||
|
final GitTagsCommand tagsCommand = new GitTagsCommand(gitContext, gpg);
|
||||||
|
final List<Tag> tags = tagsCommand.getTags();
|
||||||
|
|
||||||
|
assertThat(tags).hasSize(3);
|
||||||
|
|
||||||
|
Tag signedTag = tags.get(1);
|
||||||
|
assertThat(signedTag.getSignatures()).isNotEmpty();
|
||||||
|
assertThat(signedTag.getSignatures().get(0).getStatus()).isEqualTo(SignatureStatus.VERIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getZippedRepositoryResource() {
|
protected String getZippedRepositoryResource() {
|
||||||
return "sonia/scm/repository/spi/scm-git-spi-test-tags.zip";
|
return "sonia/scm/repository/spi/scm-git-spi-test-tags.zip";
|
||||||
|
|||||||
@@ -35,60 +35,28 @@ import sonia.scm.repository.Repository;
|
|||||||
public class AbstractCommand
|
public class AbstractCommand
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
protected final HgCommandContext context;
|
||||||
* Constructs ...
|
protected final Repository repository;
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public AbstractCommand(HgCommandContext context)
|
public AbstractCommand(HgCommandContext context)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.repository = context.getScmRepository();
|
this.repository = context.getScmRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public com.aragost.javahg.Repository open()
|
public com.aragost.javahg.Repository open()
|
||||||
{
|
{
|
||||||
return context.open();
|
return context.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public HgCommandContext getContext()
|
public HgCommandContext getContext()
|
||||||
{
|
{
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Repository getRepository()
|
public Repository getRepository()
|
||||||
{
|
{
|
||||||
return repository;
|
return repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private HgCommandContext context;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private Repository repository;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.aragost.javahg.Changeset;
|
||||||
|
import com.aragost.javahg.Repository;
|
||||||
|
import com.aragost.javahg.commands.ExecutionException;
|
||||||
|
import com.aragost.javahg.commands.PullCommand;
|
||||||
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
|
import sonia.scm.repository.work.WorkingCopy;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class AbstractWorkingCopyCommand extends AbstractCommand {
|
||||||
|
static final Pattern HG_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\](?: Error:)? (.*)");
|
||||||
|
|
||||||
|
protected final HgWorkingCopyFactory workingCopyFactory;
|
||||||
|
|
||||||
|
public AbstractWorkingCopyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory)
|
||||||
|
{
|
||||||
|
super(context);
|
||||||
|
this.workingCopyFactory = workingCopyFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<Changeset> pullChangesIntoCentralRepository(WorkingCopy<Repository, Repository> workingCopy, String branch) {
|
||||||
|
try {
|
||||||
|
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
||||||
|
workingCopyFactory.configure(pullCommand);
|
||||||
|
return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw IntegrateChangesFromWorkdirException
|
||||||
|
.withPattern(HG_MESSAGE_PATTERN)
|
||||||
|
.forMessage(context.getScmRepository(), e.getMessage());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InternalRepositoryException(getRepository(),
|
||||||
|
String.format("Could not pull changes '%s' into central repository", branch),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
import com.aragost.javahg.Changeset;
|
import com.aragost.javahg.Changeset;
|
||||||
import com.aragost.javahg.commands.CommitCommand;
|
import com.aragost.javahg.commands.CommitCommand;
|
||||||
import com.aragost.javahg.commands.PullCommand;
|
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -37,21 +36,16 @@ import sonia.scm.repository.api.BranchRequest;
|
|||||||
import sonia.scm.repository.work.WorkingCopy;
|
import sonia.scm.repository.work.WorkingCopy;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mercurial implementation of the {@link BranchCommand}.
|
* Mercurial implementation of the {@link BranchCommand}.
|
||||||
* Note that this creates an empty commit to "persist" the new branch.
|
* Note that this creates an empty commit to "persist" the new branch.
|
||||||
*/
|
*/
|
||||||
public class HgBranchCommand extends AbstractCommand implements BranchCommand {
|
public class HgBranchCommand extends AbstractWorkingCopyCommand implements BranchCommand {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(HgBranchCommand.class);
|
private static final Logger LOG = LoggerFactory.getLogger(HgBranchCommand.class);
|
||||||
|
|
||||||
private final HgWorkingCopyFactory workingCopyFactory;
|
|
||||||
|
|
||||||
HgBranchCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
|
HgBranchCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
|
||||||
super(context);
|
super(context, workingCopyFactory);
|
||||||
this.workingCopyFactory = workingCopyFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -103,15 +97,4 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pullChangesIntoCentralRepository(WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy, String branch) {
|
|
||||||
try {
|
|
||||||
PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
|
||||||
workingCopyFactory.configure(pullCommand);
|
|
||||||
pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InternalRepositoryException(getRepository(),
|
|
||||||
String.format("Could not pull changes '%s' into central repository", branch),
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import com.aragost.javahg.Changeset;
|
|||||||
import com.aragost.javahg.Repository;
|
import com.aragost.javahg.Repository;
|
||||||
import com.aragost.javahg.commands.CommitCommand;
|
import com.aragost.javahg.commands.CommitCommand;
|
||||||
import com.aragost.javahg.commands.ExecutionException;
|
import com.aragost.javahg.commands.ExecutionException;
|
||||||
import com.aragost.javahg.commands.PullCommand;
|
|
||||||
import com.aragost.javahg.commands.RemoveCommand;
|
import com.aragost.javahg.commands.RemoveCommand;
|
||||||
import com.aragost.javahg.commands.StatusCommand;
|
import com.aragost.javahg.commands.StatusCommand;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -41,20 +40,17 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
import static sonia.scm.repository.spi.UserFormatter.getUserStringFor;
|
||||||
|
|
||||||
@SuppressWarnings("java:S3252") // it is ok for javahg classes to access static method of subtype
|
@SuppressWarnings("java:S3252") // it is ok for javahg classes to access static method of subtype
|
||||||
public class HgModifyCommand implements ModifyCommand {
|
public class HgModifyCommand extends AbstractWorkingCopyCommand implements ModifyCommand {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(HgModifyCommand.class);
|
private static final Logger LOG = LoggerFactory.getLogger(HgModifyCommand.class);
|
||||||
static final Pattern HG_MESSAGE_PATTERN = Pattern.compile(".*\\[SCM\\](?: Error:)? (.*)");
|
|
||||||
|
|
||||||
private final HgCommandContext context;
|
|
||||||
private final HgWorkingCopyFactory workingCopyFactory;
|
|
||||||
|
|
||||||
public HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
|
public HgModifyCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
|
||||||
this.context = context;
|
super(context, workingCopyFactory);
|
||||||
this.workingCopyFactory = workingCopyFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -110,10 +106,10 @@ public class HgModifyCommand implements ModifyCommand {
|
|||||||
|
|
||||||
LOG.trace("commit changes in working copy");
|
LOG.trace("commit changes in working copy");
|
||||||
CommitCommand.on(workingRepository)
|
CommitCommand.on(workingRepository)
|
||||||
.user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail()))
|
.user(getUserStringFor(request.getAuthor()))
|
||||||
.message(request.getCommitMessage()).execute();
|
.message(request.getCommitMessage()).execute();
|
||||||
|
|
||||||
List<Changeset> execute = pullModifyChangesToCentralRepository(request, workingCopy);
|
List<Changeset> execute = pullChangesIntoCentralRepository(workingCopy, request.getBranch());
|
||||||
|
|
||||||
String node = execute.get(0).getNode();
|
String node = execute.get(0).getNode();
|
||||||
LOG.debug("successfully pulled changes from working copy, new node {}", node);
|
LOG.debug("successfully pulled changes from working copy, new node {}", node);
|
||||||
@@ -124,24 +120,7 @@ public class HgModifyCommand implements ModifyCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Changeset> pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy<com.aragost.javahg.Repository, com.aragost.javahg.Repository> workingCopy) {
|
private void throwInternalRepositoryException(String message, Exception e) {
|
||||||
LOG.trace("pull changes from working copy");
|
|
||||||
try {
|
|
||||||
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
|
||||||
workingCopyFactory.configure(pullCommand);
|
|
||||||
return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
throw IntegrateChangesFromWorkdirException
|
|
||||||
.withPattern(HG_MESSAGE_PATTERN)
|
|
||||||
.forMessage(context.getScmRepository(), e.getMessage());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InternalRepositoryException(context.getScmRepository(),
|
|
||||||
String.format("Could not pull modify changes from working copy to central repository for branch %s", request.getBranch()),
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String throwInternalRepositoryException(String message, Exception e) {
|
|
||||||
throw new InternalRepositoryException(context.getScmRepository(), message, e);
|
throw new InternalRepositoryException(context.getScmRepository(), message, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
Command.DIFF,
|
Command.DIFF,
|
||||||
Command.LOG,
|
Command.LOG,
|
||||||
Command.TAGS,
|
Command.TAGS,
|
||||||
|
Command.TAG,
|
||||||
Command.BRANCH,
|
Command.BRANCH,
|
||||||
Command.BRANCHES,
|
Command.BRANCHES,
|
||||||
Command.INCOMING,
|
Command.INCOMING,
|
||||||
@@ -261,4 +262,9 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider {
|
|||||||
return new HgTagsCommand(context);
|
return new HgTagsCommand(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TagCommand getTagCommand() {
|
||||||
|
return new HgTagCommand(context, handler.getWorkingCopyFactory());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.aragost.javahg.Repository;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.api.TagCreateRequest;
|
||||||
|
import sonia.scm.repository.api.TagDeleteRequest;
|
||||||
|
import sonia.scm.repository.work.WorkingCopy;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
import static sonia.scm.repository.spi.UserFormatter.getUserStringFor;
|
||||||
|
|
||||||
|
public class HgTagCommand extends AbstractWorkingCopyCommand implements TagCommand {
|
||||||
|
|
||||||
|
public static final String DEFAULT_BRANCH_NAME = "default";
|
||||||
|
|
||||||
|
public HgTagCommand(HgCommandContext context, HgWorkingCopyFactory workingCopyFactory) {
|
||||||
|
super(context, workingCopyFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tag create(TagCreateRequest request) {
|
||||||
|
try (WorkingCopy<Repository, Repository> workingCopy = workingCopyFactory.createWorkingCopy(getContext(), DEFAULT_BRANCH_NAME)) {
|
||||||
|
Repository repository = getContext().open();
|
||||||
|
String rev = request.getRevision();
|
||||||
|
if (Strings.isNullOrEmpty(rev)) {
|
||||||
|
rev = repository.tip().getNode();
|
||||||
|
}
|
||||||
|
com.aragost.javahg.commands.TagCommand.on(workingCopy.getWorkingRepository())
|
||||||
|
.rev(rev)
|
||||||
|
.user(getUserStringFor(SecurityUtils.getSubject().getPrincipals().oneByType(User.class)))
|
||||||
|
.execute(request.getName());
|
||||||
|
pullChangesIntoCentralRepository(workingCopy, DEFAULT_BRANCH_NAME);
|
||||||
|
return new Tag(request.getName(), rev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(TagDeleteRequest request) {
|
||||||
|
try (WorkingCopy<Repository, Repository> workingCopy = workingCopyFactory.createWorkingCopy(getContext(), DEFAULT_BRANCH_NAME)) {
|
||||||
|
com.aragost.javahg.commands.TagCommand.on(workingCopy.getWorkingRepository())
|
||||||
|
.user(getUserStringFor(SecurityUtils.getSubject().getPrincipals().oneByType(User.class)))
|
||||||
|
.remove()
|
||||||
|
.execute(request.getName());
|
||||||
|
|
||||||
|
pullChangesIntoCentralRepository(workingCopy, DEFAULT_BRANCH_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class HgTagsCommand extends AbstractCommand implements TagsCommand {
|
public class HgTagsCommand extends AbstractCommand implements TagsCommand {
|
||||||
|
|
||||||
|
public static final String DEFAULT_TAG_NAME = "tip";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs ...
|
* Constructs ...
|
||||||
*
|
*
|
||||||
@@ -99,7 +101,7 @@ public class HgTagsCommand extends AbstractCommand implements TagsCommand {
|
|||||||
|
|
||||||
if ((f != null) && !Strings.isNullOrEmpty(f.getName())
|
if ((f != null) && !Strings.isNullOrEmpty(f.getName())
|
||||||
&& (f.getChangeset() != null)) {
|
&& (f.getChangeset() != null)) {
|
||||||
t = new Tag(f.getName(), f.getChangeset().getNode(), f.getChangeset().getTimestamp().getDate().getTime());
|
t = new Tag(f.getName(), f.getChangeset().getNode(), f.getChangeset().getTimestamp().getDate().getTime(), !f.getName().equals(DEFAULT_TAG_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
final class UserFormatter {
|
||||||
|
private UserFormatter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getUserStringFor(User user) {
|
||||||
|
return getUserStringFor(new Person(user.getName(), user.getMail()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getUserStringFor(Person person) {
|
||||||
|
return String.format("%s <%s>", person.getName(), person.getMail());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,14 +184,14 @@ public class HgModifyCommandTest extends AbstractHgCommandTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldExtractSimpleMessage() {
|
public void shouldExtractSimpleMessage() {
|
||||||
Matcher matcher = HgModifyCommand.HG_MESSAGE_PATTERN.matcher("[SCM] This is a simple message");
|
Matcher matcher = AbstractWorkingCopyCommand.HG_MESSAGE_PATTERN.matcher("[SCM] This is a simple message");
|
||||||
matcher.matches();
|
matcher.matches();
|
||||||
assertThat(matcher.group(1)).isEqualTo("This is a simple message");
|
assertThat(matcher.group(1)).isEqualTo("This is a simple message");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldExtractErrorMessage() {
|
public void shouldExtractErrorMessage() {
|
||||||
Matcher matcher = HgModifyCommand.HG_MESSAGE_PATTERN.matcher("[SCM] Error: This is an error message");
|
Matcher matcher = AbstractWorkingCopyCommand.HG_MESSAGE_PATTERN.matcher("[SCM] Error: This is an error message");
|
||||||
matcher.matches();
|
matcher.matches();
|
||||||
assertThat(matcher.group(1)).isEqualTo("This is an error message");
|
assertThat(matcher.group(1)).isEqualTo("This is an error message");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.aragost.javahg.commands.PullCommand;
|
||||||
|
import com.google.inject.util.Providers;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import sonia.scm.repository.HgTestUtil;
|
||||||
|
import sonia.scm.repository.Tag;
|
||||||
|
import sonia.scm.repository.api.TagCreateRequest;
|
||||||
|
import sonia.scm.repository.api.TagDeleteRequest;
|
||||||
|
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
|
||||||
|
import sonia.scm.repository.work.WorkdirProvider;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
public class HgTagCommandTest extends AbstractHgCommandTestBase {
|
||||||
|
|
||||||
|
private SimpleHgWorkingCopyFactory workingCopyFactory;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initWorkingCopyFactory() {
|
||||||
|
|
||||||
|
workingCopyFactory = new SimpleHgWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())) {
|
||||||
|
@Override
|
||||||
|
public void configure(PullCommand pullCommand) {
|
||||||
|
// we do not want to configure http hooks in this unit test
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateAndDeleteTagCorrectly() {
|
||||||
|
// Create
|
||||||
|
new HgTagCommand(cmdContext, workingCopyFactory).create(new TagCreateRequest("79b6baf49711", "newtag"));
|
||||||
|
List<Tag> tags = new HgTagsCommand(cmdContext).getTags();
|
||||||
|
assertThat(tags).hasSize(2);
|
||||||
|
final Tag newTag = tags.get(1);
|
||||||
|
assertThat(newTag.getName()).isEqualTo("newtag");
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
new HgTagCommand(cmdContext, workingCopyFactory).delete(new TagDeleteRequest("newtag"));
|
||||||
|
tags = new HgTagsCommand(cmdContext).getTags();
|
||||||
|
assertThat(tags).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -94,9 +94,11 @@ class Button extends React.Component<Props> {
|
|||||||
<span className="icon is-medium">
|
<span className="icon is-medium">
|
||||||
<Icon name={icon} color="inherit" />
|
<Icon name={icon} color="inherit" />
|
||||||
</span>
|
</span>
|
||||||
<span>
|
{(label || children) && (
|
||||||
{label} {children}
|
<span>
|
||||||
</span>
|
{label} {children}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
|
|||||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||||
export { default as OverviewPageActions } from "./OverviewPageActions";
|
export { default as OverviewPageActions } from "./OverviewPageActions";
|
||||||
export { default as CardColumnGroup } from "./CardColumnGroup";
|
export { default as CardColumnGroup } from "./CardColumnGroup";
|
||||||
|
export { default as CreateTagModal } from "./modals/CreateTagModal";
|
||||||
export { default as CardColumn } from "./CardColumn";
|
export { default as CardColumn } from "./CardColumn";
|
||||||
export { default as CardColumnSmall } from "./CardColumnSmall";
|
export { default as CardColumnSmall } from "./CardColumnSmall";
|
||||||
export { default as CommaSeparatedList } from "./CommaSeparatedList";
|
export { default as CommaSeparatedList } from "./CommaSeparatedList";
|
||||||
|
|||||||
109
scm-ui/ui-components/src/modals/CreateTagModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, useEffect, useState } from "react";
|
||||||
|
import { Modal, InputField, Button, apiClient } from "@scm-manager/ui-components";
|
||||||
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
|
import { Tag } from "@scm-manager/ui-types";
|
||||||
|
import { isBranchValid } from "../validation";
|
||||||
|
|
||||||
|
type Props = WithTranslation & {
|
||||||
|
existingTagsLink: string;
|
||||||
|
tagCreationLink: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreated: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
revision: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateTagModal: FC<Props> = ({ t, onClose, tagCreationLink, existingTagsLink, onCreated, onError, revision }) => {
|
||||||
|
const [newTagName, setNewTagName] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [tagNames, setTagNames] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiClient
|
||||||
|
.get(existingTagsLink)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => setTagNames(json._embedded.tags.map((tag: Tag) => tag.name)));
|
||||||
|
}, [existingTagsLink]);
|
||||||
|
|
||||||
|
const createTag = () => {
|
||||||
|
setLoading(true);
|
||||||
|
apiClient
|
||||||
|
.post(tagCreationLink, {
|
||||||
|
revision,
|
||||||
|
name: newTagName
|
||||||
|
})
|
||||||
|
.then(onCreated)
|
||||||
|
.catch(onError)
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
let validationError = "";
|
||||||
|
|
||||||
|
if (newTagName !== "") {
|
||||||
|
if (tagNames.includes(newTagName)) {
|
||||||
|
validationError = "tags.create.form.field.name.error.exists";
|
||||||
|
} else if (!isBranchValid(newTagName)) {
|
||||||
|
validationError = "tags.create.form.field.name.error.format";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t("tags.create.title")}
|
||||||
|
active={true}
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<InputField
|
||||||
|
name="name"
|
||||||
|
label={t("tags.create.form.field.name.label")}
|
||||||
|
onChange={val => setNewTagName(val)}
|
||||||
|
value={newTagName}
|
||||||
|
validationError={!!validationError}
|
||||||
|
errorMessage={t(validationError)}
|
||||||
|
/>
|
||||||
|
<div className="mt-6">{t("tags.create.hint")}</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button action={onClose}>{t("tags.create.cancel")}</Button>
|
||||||
|
<Button
|
||||||
|
color="success"
|
||||||
|
action={() => createTag()}
|
||||||
|
loading={loading}
|
||||||
|
disabled={!!validationError || newTagName.length === 0}
|
||||||
|
>
|
||||||
|
{t("tags.create.confirm")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
closeFunction={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTranslation("repos")(CreateTagModal);
|
||||||
@@ -22,10 +22,11 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Collection, Link, Links} from "./hal";
|
import { Collection, Links } from "./hal";
|
||||||
import { Tag } from "./Tags";
|
import { Tag } from "./Tags";
|
||||||
import { Branch } from "./Branches";
|
import { Branch } from "./Branches";
|
||||||
import { Person } from "./Person";
|
import { Person } from "./Person";
|
||||||
|
import { Signature } from "./Signature";
|
||||||
|
|
||||||
export type Changeset = Collection & {
|
export type Changeset = Collection & {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -42,17 +43,6 @@ export type Changeset = Collection & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Signature = {
|
|
||||||
keyId: string;
|
|
||||||
type: string;
|
|
||||||
status: "VERIFIED" | "NOT_FOUND" | "INVALID";
|
|
||||||
owner?: string;
|
|
||||||
contacts?: Person[];
|
|
||||||
_links?: {
|
|
||||||
rawKey?: Link;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Contributor = {
|
export type Contributor = {
|
||||||
person: Person;
|
person: Person;
|
||||||
type: string;
|
type: string;
|
||||||
|
|||||||
37
scm-ui/ui-types/src/Signature.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Person } from "./Person";
|
||||||
|
import { Link } from "./hal";
|
||||||
|
|
||||||
|
export type Signature = {
|
||||||
|
keyId: string;
|
||||||
|
type: string;
|
||||||
|
status: "VERIFIED" | "NOT_FOUND" | "INVALID";
|
||||||
|
owner?: string;
|
||||||
|
contacts?: Person[];
|
||||||
|
_links?: {
|
||||||
|
rawKey?: Link;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -23,10 +23,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Links } from "./hal";
|
import { Links } from "./hal";
|
||||||
|
import { Signature } from "./Signature";
|
||||||
|
|
||||||
export type Tag = {
|
export type Tag = {
|
||||||
name: string;
|
name: string;
|
||||||
revision: string;
|
revision: string;
|
||||||
date?: Date;
|
date?: Date;
|
||||||
|
signatures: Signature[];
|
||||||
_links: Links;
|
_links: Links;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,14 +29,24 @@ export { Me } from "./Me";
|
|||||||
export { DisplayedUser, User } from "./User";
|
export { DisplayedUser, User } from "./User";
|
||||||
export { Group, Member } from "./Group";
|
export { Group, Member } from "./Group";
|
||||||
|
|
||||||
export { Repository, RepositoryCollection, RepositoryGroup, RepositoryCreation, Namespace, NamespaceCollection, RepositoryUrlImport } from "./Repositories";
|
export {
|
||||||
|
Repository,
|
||||||
|
RepositoryCollection,
|
||||||
|
RepositoryGroup,
|
||||||
|
RepositoryCreation,
|
||||||
|
Namespace,
|
||||||
|
NamespaceCollection,
|
||||||
|
RepositoryUrlImport
|
||||||
|
} from "./Repositories";
|
||||||
export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
export { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||||
|
|
||||||
export { Branch, BranchRequest } from "./Branches";
|
export { Branch, BranchRequest } from "./Branches";
|
||||||
|
|
||||||
export { Person } from "./Person";
|
export { Person } from "./Person";
|
||||||
|
|
||||||
export { Changeset, Contributor, ParentChangeset, Signature } from "./Changesets";
|
export { Changeset, Contributor, ParentChangeset } from "./Changesets";
|
||||||
|
|
||||||
|
export { Signature } from "./Signature";
|
||||||
|
|
||||||
export { AnnotatedSource, AnnotatedLine } from "./Annotate";
|
export { AnnotatedSource, AnnotatedLine } from "./Annotate";
|
||||||
|
|
||||||
|
|||||||
@@ -124,12 +124,41 @@
|
|||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"tags": "Tags"
|
"tags": "Tags"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"form": {
|
||||||
|
"field": {
|
||||||
|
"name": {
|
||||||
|
"label": "Name",
|
||||||
|
"error": {
|
||||||
|
"exists": "Dieser Tag existiert bereits in diesem Repository.",
|
||||||
|
"format": "Der Tag entspricht nicht dem korrekten Format."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Neuen Tag anlegen",
|
||||||
|
"hint": "Der Tag wird automatisch mit ihrem Standardschlüssel vom SCM-Manager signiert.",
|
||||||
|
"confirm": "Tag erstellen",
|
||||||
|
"cancel": "Abbrechen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tag": {
|
"tag": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"commit": "Commit",
|
"commit": "Commit",
|
||||||
"sources": "Sources"
|
"sources": "Sources",
|
||||||
|
"dangerZone": "Tag löschen",
|
||||||
|
"delete": {
|
||||||
|
"button": "Tag löschen",
|
||||||
|
"subtitle": "Tag löschen",
|
||||||
|
"description": "Gelöschte Tags können nicht wiederhergestellt werden.",
|
||||||
|
"confirmAlert": {
|
||||||
|
"title": "Tag löschen",
|
||||||
|
"message": "Möchten Sie den Tag \"{{tag}}\" wirklich löschen?",
|
||||||
|
"cancel": "Nein",
|
||||||
|
"submit": "Ja"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"code": {
|
"code": {
|
||||||
"sources": "Sources",
|
"sources": "Sources",
|
||||||
@@ -179,6 +208,9 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"sources": "Sources"
|
"sources": "Sources"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"create": "Tag erstellen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commit": {
|
"commit": {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
"delete": {
|
"delete": {
|
||||||
"button": "Delete branch",
|
"button": "Delete branch",
|
||||||
"subtitle": "Delete branch",
|
"subtitle": "Delete branch",
|
||||||
"description": "Deleted branches can not be restored.",
|
"description": "Deleted branches cannot be restored.",
|
||||||
"confirmAlert": {
|
"confirmAlert": {
|
||||||
"title": "Delete branch",
|
"title": "Delete branch",
|
||||||
"message": "Do you really want to delete the branch \"{{branch}}\"?",
|
"message": "Do you really want to delete the branch \"{{branch}}\"?",
|
||||||
@@ -125,12 +125,41 @@
|
|||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"tags": "Tags"
|
"tags": "Tags"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"form": {
|
||||||
|
"field": {
|
||||||
|
"name": {
|
||||||
|
"label": "Name",
|
||||||
|
"error": {
|
||||||
|
"exists": "This tag already exists in this repository.",
|
||||||
|
"format": "The tag name does not match the correct format."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Create a new tag",
|
||||||
|
"hint": "The tag will be automatically signed with your default key by the SCM-Manager.",
|
||||||
|
"confirm": "Create Tag",
|
||||||
|
"cancel": "Cancel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tag": {
|
"tag": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"commit": "Commit",
|
"commit": "Commit",
|
||||||
"sources": "Sources"
|
"sources": "Sources",
|
||||||
|
"dangerZone": "Delete tag",
|
||||||
|
"delete": {
|
||||||
|
"button": "Delete tag",
|
||||||
|
"subtitle": "Delete tag",
|
||||||
|
"description": "Deleted tag can not be restored.",
|
||||||
|
"confirmAlert": {
|
||||||
|
"title": "Delete tag",
|
||||||
|
"message": "Do you really want to delete the tag \"{{tag}}\"?",
|
||||||
|
"cancel": "No",
|
||||||
|
"submit": "Yes"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"code": {
|
"code": {
|
||||||
"sources": "Sources",
|
"sources": "Sources",
|
||||||
@@ -180,6 +209,9 @@
|
|||||||
"more": "{{count}} more",
|
"more": "{{count}} more",
|
||||||
"count": "{{count}} Contributor",
|
"count": "{{count}} Contributor",
|
||||||
"count_plural": "{{count}} Contributors"
|
"count_plural": "{{count}} Contributors"
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"create": "Create Tag"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commit": {
|
"commit": {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class BranchView extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const { repository, branch } = this.props;
|
const { repository, branch } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<BranchDetail repository={repository} branch={branch} />
|
<BranchDetail repository={repository} branch={branch} />
|
||||||
<hr />
|
<hr />
|
||||||
<div className="content">
|
<div className="content">
|
||||||
@@ -50,7 +50,7 @@ class BranchView extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<BranchDangerZone repository={repository} branch={branch} />
|
<BranchDangerZone repository={repository} branch={branch} />
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { Trans, useTranslation, WithTranslation, withTranslation } from "react-i
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
import { Changeset, ParentChangeset, Repository } from "@scm-manager/ui-types";
|
import { Changeset, Link, ParentChangeset, Repository, Tag } from "@scm-manager/ui-types";
|
||||||
import {
|
import {
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
AvatarWrapper,
|
AvatarWrapper,
|
||||||
@@ -41,7 +41,10 @@ import {
|
|||||||
FileControlFactory,
|
FileControlFactory,
|
||||||
Icon,
|
Icon,
|
||||||
Level,
|
Level,
|
||||||
SignatureIcon
|
SignatureIcon,
|
||||||
|
Tooltip,
|
||||||
|
ErrorNotification,
|
||||||
|
CreateTagModal
|
||||||
} from "@scm-manager/ui-components";
|
} from "@scm-manager/ui-components";
|
||||||
import ContributorTable from "./ContributorTable";
|
import ContributorTable from "./ContributorTable";
|
||||||
import { Link as ReactLink } from "react-router-dom";
|
import { Link as ReactLink } from "react-router-dom";
|
||||||
@@ -50,10 +53,7 @@ type Props = WithTranslation & {
|
|||||||
changeset: Changeset;
|
changeset: Changeset;
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
fileControlFactory?: FileControlFactory;
|
fileControlFactory?: FileControlFactory;
|
||||||
};
|
refetchChangeset?: () => void;
|
||||||
|
|
||||||
type State = {
|
|
||||||
collapsed: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RightMarginP = styled.p`
|
const RightMarginP = styled.p`
|
||||||
@@ -82,7 +82,7 @@ const ContributorLine = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const ContributorColumn = styled.p`
|
const ContributorColumn = styled.p`
|
||||||
flex-grow: 1;
|
flex-grow: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -108,7 +108,6 @@ const ContributorToggleLine = styled.p`
|
|||||||
|
|
||||||
const ChangesetSummary = styled.div`
|
const ChangesetSummary = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SeparatedParents = styled.div`
|
const SeparatedParents = styled.div`
|
||||||
@@ -147,7 +146,7 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
|||||||
<Icon name="angle-right" /> <ChangesetAuthor changeset={changeset} />
|
<Icon name="angle-right" /> <ChangesetAuthor changeset={changeset} />
|
||||||
</ContributorColumn>
|
</ContributorColumn>
|
||||||
{signatureIcon}
|
{signatureIcon}
|
||||||
<CountColumn className={"is-hidden-mobile"}>
|
<CountColumn className={"is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only"}>
|
||||||
(
|
(
|
||||||
<span className="has-text-link">
|
<span className="has-text-link">
|
||||||
{t("changeset.contributors.count", { count: countContributors(changeset) })}
|
{t("changeset.contributors.count", { count: countContributors(changeset) })}
|
||||||
@@ -159,109 +158,131 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
class ChangesetDetails extends React.Component<Props, State> {
|
const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory, t, refetchChangeset }) => {
|
||||||
constructor(props: Props) {
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
super(props);
|
const [isTagCreationModalVisible, setTagCreationModalVisible] = useState(false);
|
||||||
this.state = {
|
const [error, setError] = useState<Error | undefined>();
|
||||||
collapsed: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
const description = changesets.parseDescription(changeset.description);
|
||||||
const { changeset, repository, fileControlFactory, t } = this.props;
|
const id = <ChangesetId repository={repository} changeset={changeset} link={false} />;
|
||||||
const { collapsed } = this.state;
|
const date = <DateFromNow date={changeset.date} />;
|
||||||
|
const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => (
|
||||||
|
<ReactLink title={parent.id} to={parent.id} key={index}>
|
||||||
|
{parent.id.substring(0, 7)}
|
||||||
|
</ReactLink>
|
||||||
|
));
|
||||||
|
const showCreateButton = "tag" in changeset._links;
|
||||||
|
|
||||||
const description = changesets.parseDescription(changeset.description);
|
const collapseDiffs = () => {
|
||||||
const id = <ChangesetId repository={repository} changeset={changeset} link={false} />;
|
setCollapsed(!collapsed);
|
||||||
const date = <DateFromNow date={changeset.date} />;
|
|
||||||
const parents = changeset._embedded.parents.map((parent: ParentChangeset, index: number) => (
|
|
||||||
<ReactLink title={parent.id} to={parent.id} key={index}>
|
|
||||||
{parent.id.substring(0, 7)}
|
|
||||||
</ReactLink>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={classNames("content", "is-marginless")}>
|
|
||||||
<h4>
|
|
||||||
<ExtensionPoint
|
|
||||||
name="changeset.description"
|
|
||||||
props={{
|
|
||||||
changeset,
|
|
||||||
value: description.title
|
|
||||||
}}
|
|
||||||
renderAll={false}
|
|
||||||
>
|
|
||||||
<ChangesetDescription changeset={changeset} value={description.title} />
|
|
||||||
</ExtensionPoint>
|
|
||||||
</h4>
|
|
||||||
<article className="media">
|
|
||||||
<AvatarWrapper>
|
|
||||||
<RightMarginP className={classNames("image", "is-64x64")}>
|
|
||||||
<AvatarImage person={changeset.author} />
|
|
||||||
</RightMarginP>
|
|
||||||
</AvatarWrapper>
|
|
||||||
<div className="media-content">
|
|
||||||
<Contributors changeset={changeset} />
|
|
||||||
<ChangesetSummary className="is-ellipsis-overflow">
|
|
||||||
<p>
|
|
||||||
<Trans i18nKey="repos:changeset.summary" components={[id, date]} />
|
|
||||||
</p>
|
|
||||||
{parents?.length > 0 && (
|
|
||||||
<SeparatedParents>
|
|
||||||
{t("changeset.parents.label", { count: parents?.length }) + ": "}
|
|
||||||
{parents}
|
|
||||||
</SeparatedParents>
|
|
||||||
)}
|
|
||||||
</ChangesetSummary>
|
|
||||||
</div>
|
|
||||||
<div className="media-right">
|
|
||||||
<ChangesetTags changeset={changeset} />
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<p>
|
|
||||||
{description.message.split("\n").map((item, key) => {
|
|
||||||
return (
|
|
||||||
<span key={key}>
|
|
||||||
<ExtensionPoint
|
|
||||||
name="changeset.description"
|
|
||||||
props={{
|
|
||||||
changeset,
|
|
||||||
value: item
|
|
||||||
}}
|
|
||||||
renderAll={false}
|
|
||||||
>
|
|
||||||
<ChangesetDescription changeset={changeset} value={item} />
|
|
||||||
</ExtensionPoint>
|
|
||||||
<br />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BottomMarginLevel
|
|
||||||
right={
|
|
||||||
<Button
|
|
||||||
action={this.collapseDiffs}
|
|
||||||
color="default"
|
|
||||||
icon={collapsed ? "eye" : "eye-slash"}
|
|
||||||
label={t("changesets.collapseDiffs")}
|
|
||||||
reducedMobile={true}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ChangesetDiff changeset={changeset} fileControlFactory={fileControlFactory} defaultCollapse={collapsed} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
collapseDiffs = () => {
|
|
||||||
this.setState(state => ({
|
|
||||||
collapsed: !state.collapsed
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorNotification error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classNames("content", "is-marginless")}>
|
||||||
|
<h4>
|
||||||
|
<ExtensionPoint
|
||||||
|
name="changeset.description"
|
||||||
|
props={{
|
||||||
|
changeset,
|
||||||
|
value: description.title
|
||||||
|
}}
|
||||||
|
renderAll={false}
|
||||||
|
>
|
||||||
|
<ChangesetDescription changeset={changeset} value={description.title} />
|
||||||
|
</ExtensionPoint>
|
||||||
|
</h4>
|
||||||
|
<article className="media">
|
||||||
|
<AvatarWrapper>
|
||||||
|
<RightMarginP className={classNames("image", "is-64x64")}>
|
||||||
|
<AvatarImage person={changeset.author} />
|
||||||
|
</RightMarginP>
|
||||||
|
</AvatarWrapper>
|
||||||
|
<div className="media-content">
|
||||||
|
<Contributors changeset={changeset} />
|
||||||
|
<ChangesetSummary className="is-ellipsis-overflow">
|
||||||
|
<p>
|
||||||
|
<Trans i18nKey="repos:changeset.summary" components={[id, date]} />
|
||||||
|
</p>
|
||||||
|
{parents?.length > 0 && (
|
||||||
|
<SeparatedParents>
|
||||||
|
{t("changeset.parents.label", { count: parents?.length }) + ": "}
|
||||||
|
{parents}
|
||||||
|
</SeparatedParents>
|
||||||
|
)}
|
||||||
|
</ChangesetSummary>
|
||||||
|
</div>
|
||||||
|
<div className="media-right">
|
||||||
|
<ChangesetTags changeset={changeset} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCreateButton && (
|
||||||
|
<div className="media-right">
|
||||||
|
<Tooltip message={t("changeset.tag.create")} location="top">
|
||||||
|
<Button
|
||||||
|
color="success"
|
||||||
|
className="tag"
|
||||||
|
label={(changeset._embedded["tags"]?.length === 0 && t("changeset.tag.create")) || ""}
|
||||||
|
icon="plus"
|
||||||
|
action={() => setTagCreationModalVisible(true)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isTagCreationModalVisible && (
|
||||||
|
<CreateTagModal
|
||||||
|
revision={changeset.id}
|
||||||
|
onClose={() => setTagCreationModalVisible(false)}
|
||||||
|
onCreated={() => {
|
||||||
|
refetchChangeset?.();
|
||||||
|
setTagCreationModalVisible(false);
|
||||||
|
}}
|
||||||
|
onError={setError}
|
||||||
|
tagCreationLink={(changeset._links["tag"] as Link).href}
|
||||||
|
existingTagsLink={(repository._links["tags"] as Link).href}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
<p>
|
||||||
|
{description.message.split("\n").map((item, key) => {
|
||||||
|
return (
|
||||||
|
<span key={key}>
|
||||||
|
<ExtensionPoint
|
||||||
|
name="changeset.description"
|
||||||
|
props={{
|
||||||
|
changeset,
|
||||||
|
value: item
|
||||||
|
}}
|
||||||
|
renderAll={false}
|
||||||
|
>
|
||||||
|
<ChangesetDescription changeset={changeset} value={item} />
|
||||||
|
</ExtensionPoint>
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<BottomMarginLevel
|
||||||
|
right={
|
||||||
|
<Button
|
||||||
|
action={collapseDiffs}
|
||||||
|
color="default"
|
||||||
|
icon={collapsed ? "eye" : "eye-slash"}
|
||||||
|
label={t("changesets.collapseDiffs")}
|
||||||
|
reducedMobile={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ChangesetDiff changeset={changeset} fileControlFactory={fileControlFactory} defaultCollapse={collapsed} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default withTranslation("repos")(ChangesetDetails);
|
export default withTranslation("repos")(ChangesetDetails);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { WithTranslation, withTranslation } from "react-i18next";
|
|||||||
import { Changeset, Repository } from "@scm-manager/ui-types";
|
import { Changeset, Repository } from "@scm-manager/ui-types";
|
||||||
import { ErrorPage, Loading } from "@scm-manager/ui-components";
|
import { ErrorPage, Loading } from "@scm-manager/ui-components";
|
||||||
import {
|
import {
|
||||||
|
fetchChangeset,
|
||||||
fetchChangesetIfNeeded,
|
fetchChangesetIfNeeded,
|
||||||
getChangeset,
|
getChangeset,
|
||||||
getFetchChangesetFailure,
|
getFetchChangesetFailure,
|
||||||
@@ -45,6 +46,7 @@ type Props = WithTranslation & {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
fetchChangesetIfNeeded: (repository: Repository, id: string) => void;
|
fetchChangesetIfNeeded: (repository: Repository, id: string) => void;
|
||||||
|
refetchChangeset: (repository: Repository, id: string) => void;
|
||||||
match: any;
|
match: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,7 +64,7 @@ class ChangesetView extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { changeset, loading, error, t, repository, fileControlFactoryFactory } = this.props;
|
const { changeset, loading, error, t, repository, fileControlFactoryFactory, refetchChangeset } = this.props;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorPage title={t("changesets.errorTitle")} subtitle={t("changesets.errorSubtitle")} error={error} />;
|
return <ErrorPage title={t("changesets.errorTitle")} subtitle={t("changesets.errorSubtitle")} error={error} />;
|
||||||
@@ -75,6 +77,7 @@ class ChangesetView extends React.Component<Props> {
|
|||||||
changeset={changeset}
|
changeset={changeset}
|
||||||
repository={repository}
|
repository={repository}
|
||||||
fileControlFactory={fileControlFactoryFactory && fileControlFactoryFactory(changeset)}
|
fileControlFactory={fileControlFactoryFactory && fileControlFactoryFactory(changeset)}
|
||||||
|
refetchChangeset={() => refetchChangeset(repository, changeset.id)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,6 +101,9 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||||||
return {
|
return {
|
||||||
fetchChangesetIfNeeded: (repository: Repository, id: string) => {
|
fetchChangesetIfNeeded: (repository: Repository, id: string) => {
|
||||||
dispatch(fetchChangesetIfNeeded(repository, id));
|
dispatch(fetchChangesetIfNeeded(repository, id));
|
||||||
|
},
|
||||||
|
refetchChangeset: (repository: Repository, id: string) => {
|
||||||
|
dispatch(fetchChangeset(repository, id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Repository, Tag } from "@scm-manager/ui-types";
|
import { Repository, Tag } from "@scm-manager/ui-types";
|
||||||
import { DateFromNow } from "@scm-manager/ui-components";
|
import { DateFromNow, SignatureIcon } from "@scm-manager/ui-components";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import TagButtonGroup from "./TagButtonGroup";
|
import TagButtonGroup from "./TagButtonGroup";
|
||||||
|
|
||||||
@@ -58,9 +58,10 @@ const TagDetail: FC<Props> = ({ tag, repository }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="media">
|
<div className="media">
|
||||||
<FlexRow className="media-content subtitle">
|
<FlexRow className="media-content">
|
||||||
<Label>{t("tag.name") + ": "} </Label> {tag.name}
|
<Label className="subtitle has-text-weight-bold has-text-black">{t("tag.name") + ": "} </Label> <span className="subtitle">{tag.name}</span>
|
||||||
<Created className="is-ellipsis-overflow">
|
<SignatureIcon signatures={tag.signatures} className="ml-2 mb-5" />
|
||||||
|
<Created className="is-ellipsis-overflow mb-5">
|
||||||
{t("tags.overview.created")} <Date date={tag.date} className="has-text-grey" />
|
{t("tags.overview.created")} <Date date={tag.date} className="has-text-grey" />
|
||||||
</Created>
|
</Created>
|
||||||
</FlexRow>
|
</FlexRow>
|
||||||
|
|||||||
@@ -24,14 +24,16 @@
|
|||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { Tag } from "@scm-manager/ui-types";
|
import { Tag, Link } from "@scm-manager/ui-types";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { DateFromNow } from "@scm-manager/ui-components";
|
import { DateFromNow, Icon } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tag: Tag;
|
tag: Tag;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
onDelete: (tag: Tag) => void;
|
||||||
|
// deleting: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Created = styled.span`
|
const Created = styled.span`
|
||||||
@@ -39,20 +41,32 @@ const Created = styled.span`
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TagRow: FC<Props> = ({ tag, baseUrl }) => {
|
const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
|
||||||
|
let deleteButton;
|
||||||
|
if ((tag?._links?.delete as Link)?.href) {
|
||||||
|
deleteButton = (
|
||||||
|
<a className="level-item" onClick={() => onDelete(tag)}>
|
||||||
|
<span className="icon is-small">
|
||||||
|
<Icon name="trash" className="fas" title={t("tag.delete.button")} />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`;
|
const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`;
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<Link to={to} title={tag.name}>
|
<RouterLink to={to} title={tag.name}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
<Created className="has-text-grey is-ellipsis-overflow">
|
<Created className="has-text-grey is-ellipsis-overflow">
|
||||||
{t("tags.overview.created")} <DateFromNow date={tag.date} />
|
{t("tags.overview.created")} <DateFromNow date={tag.date} />
|
||||||
</Created>
|
</Created>
|
||||||
</Link>
|
</RouterLink>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="is-darker">{deleteButton}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,38 +22,83 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { Tag } from "@scm-manager/ui-types";
|
import { Link, Tag } from "@scm-manager/ui-types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import TagRow from "./TagRow";
|
import TagRow from "./TagRow";
|
||||||
|
import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
|
fetchTags: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TagTable: FC<Props> = ({ baseUrl, tags }) => {
|
const TagTable: FC<Props> = ({ baseUrl, tags, fetchTags }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | undefined>();
|
||||||
|
const [tagToBeDeleted, setTagToBeDeleted] = useState<Tag | undefined>();
|
||||||
|
|
||||||
|
const onDelete = (tag: Tag) => {
|
||||||
|
setTagToBeDeleted(tag);
|
||||||
|
setShowConfirmAlert(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortDelete = () => {
|
||||||
|
setTagToBeDeleted(undefined);
|
||||||
|
setShowConfirmAlert(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTag = () => {
|
||||||
|
apiClient
|
||||||
|
.delete((tagToBeDeleted?._links.delete as Link).href)
|
||||||
|
.then(() => fetchTags())
|
||||||
|
.catch(setError);
|
||||||
|
};
|
||||||
|
|
||||||
const renderRow = () => {
|
const renderRow = () => {
|
||||||
let rowContent = null;
|
let rowContent = null;
|
||||||
if (tags) {
|
if (tags) {
|
||||||
rowContent = tags.map((tag, index) => {
|
rowContent = tags.map((tag, index) => {
|
||||||
return <TagRow key={index} baseUrl={baseUrl} tag={tag} />;
|
return <TagRow key={index} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return rowContent;
|
return rowContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmAlert = (
|
||||||
|
<ConfirmAlert
|
||||||
|
title={t("tag.delete.confirmAlert.title")}
|
||||||
|
message={t("tag.delete.confirmAlert.message", { tag: tagToBeDeleted?.name })}
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
className: "is-outlined",
|
||||||
|
label: t("tag.delete.confirmAlert.submit"),
|
||||||
|
onClick: () => deleteTag()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("tag.delete.confirmAlert.cancel"),
|
||||||
|
onClick: () => abortDelete()
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
close={() => abortDelete()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="card-table table is-hoverable is-fullwidth is-word-break">
|
<>
|
||||||
<thead>
|
{showConfirmAlert && confirmAlert}
|
||||||
<tr>
|
{error && <ErrorNotification error={error} />}
|
||||||
<th>{t("tags.table.tags")}</th>
|
<table className="card-table table is-hoverable is-fullwidth is-word-break">
|
||||||
</tr>
|
<thead>
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>{renderRow()}</tbody>
|
<th>{t("tags.table.tags")}</th>
|
||||||
</table>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{renderRow()}</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import React, { FC } from "react";
|
|||||||
import { Repository, Tag } from "@scm-manager/ui-types";
|
import { Repository, Tag } from "@scm-manager/ui-types";
|
||||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||||
import TagDetail from "./TagDetail";
|
import TagDetail from "./TagDetail";
|
||||||
|
import TagDangerZone from "../container/TagDangerZone";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -47,6 +48,7 @@ const TagView: FC<Props> = ({ repository, tag }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<TagDangerZone repository={repository} tag={tag} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
93
scm-ui/ui-webapp/src/repos/tags/container/DeleteTag.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { apiClient, ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
|
||||||
|
import { Link, Repository, Tag } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
repository: Repository;
|
||||||
|
tag: Tag;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteTag: FC<Props> = ({ tag, repository }) => {
|
||||||
|
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | undefined>();
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const deleteBranch = () => {
|
||||||
|
apiClient
|
||||||
|
.delete((tag._links.delete as Link).href)
|
||||||
|
.then(() => history.push(`/repo/${repository.namespace}/${repository.name}/tags/`))
|
||||||
|
.catch(setError);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tag._links.delete) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirmAlert = null;
|
||||||
|
if (showConfirmAlert) {
|
||||||
|
confirmAlert = (
|
||||||
|
<ConfirmAlert
|
||||||
|
title={t("tag.delete.confirmAlert.title")}
|
||||||
|
message={t("tag.delete.confirmAlert.message", { tag: tag.name })}
|
||||||
|
buttons={[
|
||||||
|
{
|
||||||
|
className: "is-outlined",
|
||||||
|
label: t("tag.delete.confirmAlert.submit"),
|
||||||
|
onClick: () => deleteBranch()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("tag.delete.confirmAlert.cancel"),
|
||||||
|
onClick: () => null
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
close={() => setShowConfirmAlert(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ErrorNotification error={error} />
|
||||||
|
{showConfirmAlert && confirmAlert}
|
||||||
|
<Level
|
||||||
|
left={
|
||||||
|
<p>
|
||||||
|
<strong>{t("tag.delete.subtitle")}</strong>
|
||||||
|
<br />
|
||||||
|
{t("tag.delete.description")}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
right={<DeleteButton label={t("tag.delete.button")} action={() => setShowConfirmAlert(true)} />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteTag;
|
||||||
59
scm-ui/ui-webapp/src/repos/tags/container/TagDangerZone.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Repository, Tag } from "@scm-manager/ui-types";
|
||||||
|
import { Subtitle } from "@scm-manager/ui-components";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DangerZoneContainer } from "../../containers/RepositoryDangerZone";
|
||||||
|
import DeleteTag from "./DeleteTag";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
repository: Repository;
|
||||||
|
tag: Tag;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TagDangerZone: FC<Props> = ({ repository, tag }) => {
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
|
|
||||||
|
const dangerZone = [];
|
||||||
|
|
||||||
|
if (tag?._links?.delete) {
|
||||||
|
dangerZone.push(<DeleteTag repository={repository} tag={tag} key={dangerZone.length} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dangerZone.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<Subtitle subtitle={t("tag.dangerZone")} />
|
||||||
|
<DangerZoneContainer>{dangerZone}</DangerZoneContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagDangerZone;
|
||||||
@@ -40,7 +40,7 @@ const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
|||||||
const [error, setError] = useState<Error | undefined>(undefined);
|
const [error, setError] = useState<Error | undefined>(undefined);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchTags = () => {
|
||||||
const link = (repository._links?.tags as Link)?.href;
|
const link = (repository._links?.tags as Link)?.href;
|
||||||
if (link) {
|
if (link) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -51,12 +51,16 @@ const TagsOverview: FC<Props> = ({ repository, baseUrl }) => {
|
|||||||
.then(() => setLoading(false))
|
.then(() => setLoading(false))
|
||||||
.catch(setError);
|
.catch(setError);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTags();
|
||||||
}, [repository]);
|
}, [repository]);
|
||||||
|
|
||||||
const renderTagsTable = () => {
|
const renderTagsTable = () => {
|
||||||
if (!loading && tags?.length > 0) {
|
if (!loading && tags?.length > 0) {
|
||||||
orderTags(tags);
|
orderTags(tags);
|
||||||
return <TagTable baseUrl={baseUrl} tags={tags} />;
|
return <TagTable baseUrl={baseUrl} tags={tags} fetchTags={fetchTags} />;
|
||||||
}
|
}
|
||||||
return <Notification type="info">{t("tags.overview.noTags")}</Notification>;
|
return <Notification type="info">{t("tags.overview.noTags")}</Notification>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ public class BranchRootResource {
|
|||||||
@PathParam("name") String name,
|
@PathParam("name") String name,
|
||||||
@PathParam("branch") String branch) {
|
@PathParam("branch") String branch) {
|
||||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||||
RepositoryPermissions.modify(repositoryService.getRepository()).check();
|
RepositoryPermissions.push(repositoryService.getRepository()).check();
|
||||||
|
|
||||||
Optional<Branch> branchToBeDeleted = repositoryService.getBranchesCommand().getBranches().getBranches().stream()
|
Optional<Branch> branchToBeDeleted = repositoryService.getBranchesCommand().getBranches().getBranches().stream()
|
||||||
.filter(b -> b.getName().equalsIgnoreCase(branch))
|
.filter(b -> b.getName().equalsIgnoreCase(branch))
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import sonia.scm.repository.Changeset;
|
|||||||
import sonia.scm.repository.Contributor;
|
import sonia.scm.repository.Contributor;
|
||||||
import sonia.scm.repository.Person;
|
import sonia.scm.repository.Person;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
import sonia.scm.repository.Signature;
|
import sonia.scm.repository.Signature;
|
||||||
import sonia.scm.repository.api.Command;
|
import sonia.scm.repository.api.Command;
|
||||||
import sonia.scm.repository.api.RepositoryService;
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
@@ -116,6 +117,9 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
|
|||||||
if (repositoryService.isSupported(Command.TAGS)) {
|
if (repositoryService.isSupported(Command.TAGS)) {
|
||||||
embeddedBuilder.with("tags", tagCollectionToDtoMapper.getMinimalEmbeddedTagDtoList(namespace, name, source.getTags()));
|
embeddedBuilder.with("tags", tagCollectionToDtoMapper.getMinimalEmbeddedTagDtoList(namespace, name, source.getTags()));
|
||||||
}
|
}
|
||||||
|
if (repositoryService.isSupported(Command.TAG) && RepositoryPermissions.push(repository).isPermitted()) {
|
||||||
|
linksBuilder.single(link("tag", resourceLinks.tag().create(namespace, name)));
|
||||||
|
}
|
||||||
if (repositoryService.isSupported(Command.BRANCHES)) {
|
if (repositoryService.isSupported(Command.BRANCHES)) {
|
||||||
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository,
|
embeddedBuilder.with("branches", branchCollectionToDtoMapper.getBranchDtoList(repository,
|
||||||
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
|
getListOfObjects(source.getBranches(), branchName -> Branch.normalBranch(branchName, source.getId()))));
|
||||||
|
|||||||
@@ -451,6 +451,14 @@ class ResourceLinks {
|
|||||||
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(tagName).href();
|
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(tagName).href();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String delete(String namespace, String name, String tagName) {
|
||||||
|
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("delete").parameters(tagName).href();
|
||||||
|
}
|
||||||
|
|
||||||
|
String create(String namespace, String name) {
|
||||||
|
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("create").parameters().href();
|
||||||
|
}
|
||||||
|
|
||||||
String all(String namespace, String name) {
|
String all(String namespace, String name) {
|
||||||
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href();
|
return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("getAll").parameters().href();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import com.google.inject.Inject;
|
|||||||
import de.otto.edison.hal.Embedded;
|
import de.otto.edison.hal.Embedded;
|
||||||
import de.otto.edison.hal.HalRepresentation;
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
import de.otto.edison.hal.Links;
|
import de.otto.edison.hal.Links;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -40,7 +40,6 @@ import static java.util.stream.Collectors.toList;
|
|||||||
|
|
||||||
public class TagCollectionToDtoMapper {
|
public class TagCollectionToDtoMapper {
|
||||||
|
|
||||||
|
|
||||||
private final ResourceLinks resourceLinks;
|
private final ResourceLinks resourceLinks;
|
||||||
private final TagToTagDtoMapper tagToTagDtoMapper;
|
private final TagToTagDtoMapper tagToTagDtoMapper;
|
||||||
|
|
||||||
@@ -50,12 +49,12 @@ public class TagCollectionToDtoMapper {
|
|||||||
this.tagToTagDtoMapper = tagToTagDtoMapper;
|
this.tagToTagDtoMapper = tagToTagDtoMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HalRepresentation map(String namespace, String name, Collection<Tag> tags) {
|
public HalRepresentation map(Collection<Tag> tags, Repository repository) {
|
||||||
return new HalRepresentation(createLinks(namespace, name), embedDtos(getTagDtoList(namespace, name, tags)));
|
return new HalRepresentation(createLinks(repository.getNamespace(), repository.getName()), embedDtos(getTagDtoList(tags, repository)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TagDto> getTagDtoList(String namespace, String name, Collection<Tag> tags) {
|
public List<TagDto> getTagDtoList(Collection<Tag> tags, Repository repository) {
|
||||||
return tags.stream().map(tag -> tagToTagDtoMapper.map(tag, new NamespaceAndName(namespace, name))).collect(toList());
|
return tags.stream().map(tag -> tagToTagDtoMapper.map(tag, repository)).collect(toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TagDto> getMinimalEmbeddedTagDtoList(String namespace, String name, Collection<String> tags) {
|
public List<TagDto> getMinimalEmbeddedTagDtoList(String namespace, String name, Collection<String> tags) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -46,6 +47,8 @@ public class TagDto extends HalRepresentation {
|
|||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
private Instant date;
|
private Instant date;
|
||||||
|
|
||||||
|
private List<SignatureDto> signatures;
|
||||||
|
|
||||||
TagDto(Links links) {
|
TagDto(Links links) {
|
||||||
super(links);
|
super(links);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
|
||||||
|
import static sonia.scm.repository.Tag.VALID_REV;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class TagRequestDto {
|
||||||
|
@Pattern(regexp = VALID_REV)
|
||||||
|
@NotEmpty
|
||||||
|
@Length(min = 1, max = 100)
|
||||||
|
private String revision;
|
||||||
|
|
||||||
|
@Pattern(regexp = VALID_BRANCH_NAMES)
|
||||||
|
@NotEmpty
|
||||||
|
@Length(min = 1, max = 100)
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
@@ -21,12 +21,15 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.headers.Header;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
@@ -36,16 +39,22 @@ import sonia.scm.repository.Tag;
|
|||||||
import sonia.scm.repository.Tags;
|
import sonia.scm.repository.Tags;
|
||||||
import sonia.scm.repository.api.RepositoryService;
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
|
import sonia.scm.repository.api.TagCommandBuilder;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.ws.rs.DELETE;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import static sonia.scm.AlreadyExistsException.alreadyExists;
|
||||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
import static sonia.scm.NotFoundException.notFound;
|
import static sonia.scm.NotFoundException.notFound;
|
||||||
|
|
||||||
@@ -54,14 +63,17 @@ public class TagRootResource {
|
|||||||
private final RepositoryServiceFactory serviceFactory;
|
private final RepositoryServiceFactory serviceFactory;
|
||||||
private final TagCollectionToDtoMapper tagCollectionToDtoMapper;
|
private final TagCollectionToDtoMapper tagCollectionToDtoMapper;
|
||||||
private final TagToTagDtoMapper tagToTagDtoMapper;
|
private final TagToTagDtoMapper tagToTagDtoMapper;
|
||||||
|
private final ResourceLinks resourceLinks;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public TagRootResource(RepositoryServiceFactory serviceFactory,
|
public TagRootResource(RepositoryServiceFactory serviceFactory,
|
||||||
TagCollectionToDtoMapper tagCollectionToDtoMapper,
|
TagCollectionToDtoMapper tagCollectionToDtoMapper,
|
||||||
TagToTagDtoMapper tagToTagDtoMapper) {
|
TagToTagDtoMapper tagToTagDtoMapper,
|
||||||
|
ResourceLinks resourceLinks) {
|
||||||
this.serviceFactory = serviceFactory;
|
this.serviceFactory = serviceFactory;
|
||||||
this.tagCollectionToDtoMapper = tagCollectionToDtoMapper;
|
this.tagCollectionToDtoMapper = tagCollectionToDtoMapper;
|
||||||
this.tagToTagDtoMapper = tagToTagDtoMapper;
|
this.tagToTagDtoMapper = tagToTagDtoMapper;
|
||||||
|
this.resourceLinks = resourceLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -89,7 +101,7 @@ public class TagRootResource {
|
|||||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||||
Tags tags = getTags(repositoryService);
|
Tags tags = getTags(repositoryService);
|
||||||
if (tags != null && tags.getTags() != null) {
|
if (tags != null && tags.getTags() != null) {
|
||||||
return Response.ok(tagCollectionToDtoMapper.map(namespace, name, tags.getTags())).build();
|
return Response.ok(tagCollectionToDtoMapper.map(tags.getTags(), repositoryService.getRepository())).build();
|
||||||
} else {
|
} else {
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
.entity("Error on getting tag from repository.")
|
.entity("Error on getting tag from repository.")
|
||||||
@@ -98,6 +110,66 @@ public class TagRootResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("")
|
||||||
|
@Produces(VndMediaType.TAG_REQUEST)
|
||||||
|
@Operation(summary = "Create tag",
|
||||||
|
description = "Creates a new tag.",
|
||||||
|
tags = "Repository",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
content = @Content(
|
||||||
|
mediaType = VndMediaType.TAG_REQUEST,
|
||||||
|
schema = @Schema(implementation = TagRequestDto.class),
|
||||||
|
examples = @ExampleObject(
|
||||||
|
name = "Create a new tag for a revision",
|
||||||
|
value = "{\n \"revision\":\"734713bc047d87bf7eac9674765ae793478c50d3\",\n \"name\":\"v1.1.0\"\n}",
|
||||||
|
summary = "Create a tag"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "201",
|
||||||
|
description = "create success",
|
||||||
|
headers = @Header(
|
||||||
|
name = "Location",
|
||||||
|
description = "uri to the created tag",
|
||||||
|
schema = @Schema(type = "string")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||||
|
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "not found, no tag with the specified name available in the repository",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
|
))
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "internal server error",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
|
))
|
||||||
|
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid TagRequestDto tagRequest) throws IOException {
|
||||||
|
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||||
|
String revision = tagRequest.getRevision();
|
||||||
|
String tagName = tagRequest.getName();
|
||||||
|
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||||
|
if (tagExists(tagName, repositoryService)) {
|
||||||
|
throw alreadyExists(entity(Tag.class, tagName).in(repositoryService.getRepository()));
|
||||||
|
}
|
||||||
|
Repository repository = repositoryService.getRepository();
|
||||||
|
RepositoryPermissions.push(repository).check();
|
||||||
|
TagCommandBuilder tagCommandBuilder = repositoryService.getTagCommand();
|
||||||
|
final Tag newTag = tagCommandBuilder.create()
|
||||||
|
.setRevision(revision)
|
||||||
|
.setName(tagName)
|
||||||
|
.execute();
|
||||||
|
return Response.created(URI.create(resourceLinks.tag().self(namespace, name, newTag.getName()))).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("{tagName}")
|
@Path("{tagName}")
|
||||||
@@ -136,7 +208,7 @@ public class TagRootResource {
|
|||||||
.filter(t -> tagName.equals(t.getName()))
|
.filter(t -> tagName.equals(t.getName()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> createNotFoundException(namespace, name, tagName));
|
.orElseThrow(() -> createNotFoundException(namespace, name, tagName));
|
||||||
return Response.ok(tagToTagDtoMapper.map(tag, namespaceAndName)).build();
|
return Response.ok(tagToTagDtoMapper.map(tag, repositoryService.getRepository())).build();
|
||||||
} else {
|
} else {
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
.entity("Error on getting tag from repository.")
|
.entity("Error on getting tag from repository.")
|
||||||
@@ -145,6 +217,45 @@ public class TagRootResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{tagName}")
|
||||||
|
@Produces(VndMediaType.TAG)
|
||||||
|
@Operation(summary = "Delete tag", description = "Deletes the tag provided in the path", tags = "Repository")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "success"
|
||||||
|
)
|
||||||
|
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||||
|
@ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the tags")
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "404",
|
||||||
|
description = "not found, no tag with the specified name available in the repository",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
|
))
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "500",
|
||||||
|
description = "internal server error",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = VndMediaType.ERROR_TYPE,
|
||||||
|
schema = @Schema(implementation = ErrorDto.class)
|
||||||
|
))
|
||||||
|
public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException {
|
||||||
|
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||||
|
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||||
|
RepositoryPermissions.push(repositoryService.getRepository()).check();
|
||||||
|
|
||||||
|
if (tagExists(tagName, repositoryService)) {
|
||||||
|
repositoryService.getTagCommand().delete()
|
||||||
|
.setName(tagName)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private NotFoundException createNotFoundException(String namespace, String name, String tagName) {
|
private NotFoundException createNotFoundException(String namespace, String name, String tagName) {
|
||||||
return notFound(entity("Tag", tagName).in("Repository", namespace + "/" + name));
|
return notFound(entity("Tag", tagName).in("Repository", namespace + "/" + name));
|
||||||
}
|
}
|
||||||
@@ -155,5 +266,9 @@ public class TagRootResource {
|
|||||||
return repositoryService.getTagsCommand().getTags();
|
return repositoryService.getTagsCommand().getTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean tagExists(String tagName, RepositoryService repositoryService) throws IOException {
|
||||||
|
return getTags(repositoryService)
|
||||||
|
.getTagByName(tagName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ import org.mapstruct.Mapper;
|
|||||||
import org.mapstruct.Mapping;
|
import org.mapstruct.Mapping;
|
||||||
import org.mapstruct.Named;
|
import org.mapstruct.Named;
|
||||||
import org.mapstruct.ObjectFactory;
|
import org.mapstruct.ObjectFactory;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
import sonia.scm.web.EdisonHalAppender;
|
import sonia.scm.web.EdisonHalAppender;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -52,17 +52,23 @@ public abstract class TagToTagDtoMapper extends HalAppenderMapper {
|
|||||||
|
|
||||||
@Mapping(target = "date", source = "date", qualifiedByName = "mapDate")
|
@Mapping(target = "date", source = "date", qualifiedByName = "mapDate")
|
||||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||||
public abstract TagDto map(Tag tag, @Context NamespaceAndName namespaceAndName);
|
@Mapping(target = "signatures")
|
||||||
|
public abstract TagDto map(Tag tag, @Context Repository repository);
|
||||||
|
|
||||||
@ObjectFactory
|
@ObjectFactory
|
||||||
TagDto createDto(@Context NamespaceAndName namespaceAndName, Tag tag) {
|
TagDto createDto(@Context Repository repository, Tag tag) {
|
||||||
Links.Builder linksBuilder = linkingTo()
|
Links.Builder linksBuilder = linkingTo()
|
||||||
.self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), tag.getName()))
|
.self(resourceLinks.tag().self(repository.getNamespace(), repository.getName(), tag.getName()))
|
||||||
.single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), tag.getRevision())))
|
.single(link("sources", resourceLinks.source().self(repository.getNamespace(), repository.getName(), tag.getRevision())))
|
||||||
.single(link("changeset", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), tag.getRevision())));
|
.single(link("changeset", resourceLinks.changeset().self(repository.getNamespace(), repository.getName(), tag.getRevision())));
|
||||||
|
|
||||||
|
if (tag.getDeletable() && RepositoryPermissions.push(repository).isPermitted()) {
|
||||||
|
linksBuilder
|
||||||
|
.single(link("delete", resourceLinks.tag().delete(repository.getNamespace(), repository.getName(), tag.getName())));
|
||||||
|
}
|
||||||
|
|
||||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||||
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), tag, namespaceAndName);
|
applyEnrichers(new EdisonHalAppender(linksBuilder, embeddedBuilder), tag, repository);
|
||||||
|
|
||||||
return new TagDto(linksBuilder.build(), embeddedBuilder.build());
|
return new TagDto(linksBuilder.build(), embeddedBuilder.build());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import com.google.inject.util.Providers;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.apache.shiro.subject.support.SubjectThreadState;
|
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||||
import org.apache.shiro.util.ThreadContext;
|
import org.apache.shiro.util.ThreadContext;
|
||||||
@@ -46,17 +46,24 @@ import sonia.scm.repository.Tag;
|
|||||||
import sonia.scm.repository.Tags;
|
import sonia.scm.repository.Tags;
|
||||||
import sonia.scm.repository.api.RepositoryService;
|
import sonia.scm.repository.api.RepositoryService;
|
||||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||||
|
import sonia.scm.repository.api.TagCommandBuilder;
|
||||||
import sonia.scm.repository.api.TagsCommandBuilder;
|
import sonia.scm.repository.api.TagsCommandBuilder;
|
||||||
import sonia.scm.web.RestDispatcher;
|
import sonia.scm.web.RestDispatcher;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -79,6 +86,12 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
|||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private TagsCommandBuilder tagsCommandBuilder;
|
private TagsCommandBuilder tagsCommandBuilder;
|
||||||
|
@Mock
|
||||||
|
private TagCommandBuilder tagCommandBuilder;
|
||||||
|
@Mock
|
||||||
|
private TagCommandBuilder.TagCreateCommandBuilder tagCreateCommandBuilder;
|
||||||
|
@Mock
|
||||||
|
private TagCommandBuilder.TagDeleteCommandBuilder tagDeleteCommandBuilder;
|
||||||
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
|
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
@@ -89,17 +102,21 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
|||||||
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void prepareEnvironment() throws Exception {
|
public void prepareEnvironment() {
|
||||||
tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper);
|
tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper);
|
||||||
tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper);
|
tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper, resourceLinks);
|
||||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
|
||||||
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
|
when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);
|
||||||
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||||
when(repositoryService.getTagsCommand()).thenReturn(tagsCommandBuilder);
|
when(repositoryService.getTagsCommand()).thenReturn(tagsCommandBuilder);
|
||||||
|
when(repositoryService.getTagCommand()).thenReturn(tagCommandBuilder);
|
||||||
subjectThreadState.bind();
|
subjectThreadState.bind();
|
||||||
ThreadContext.bind(subject);
|
ThreadContext.bind(subject);
|
||||||
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
when(subject.isPermitted(any(String.class))).thenReturn(true);
|
||||||
|
when(tagCreateCommandBuilder.setName(any())).thenReturn(tagCreateCommandBuilder);
|
||||||
|
when(tagCreateCommandBuilder.setRevision(any())).thenReturn(tagCreateCommandBuilder);
|
||||||
|
when(tagDeleteCommandBuilder.setName(any())).thenReturn(tagDeleteCommandBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -211,4 +228,116 @@ public class TagRootResourceTest extends RepositoryTestBase {
|
|||||||
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
|
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2)));
|
||||||
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2)));
|
assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCreateTag() throws URISyntaxException, IOException {
|
||||||
|
Tags tags = new Tags();
|
||||||
|
tags.setTags(Lists.emptyList());
|
||||||
|
when(tagsCommandBuilder.getTags()).thenReturn(tags);
|
||||||
|
when(tagCommandBuilder.create()).thenReturn(tagCreateCommandBuilder);
|
||||||
|
when(tagCreateCommandBuilder.execute()).thenReturn(new Tag("newtag", "592d797cd36432e591416e8b2b98154f4f163411"));
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(TAG_URL)
|
||||||
|
.content("{\"name\": \"newtag\",\"revision\":\"592d797cd36432e591416e8b2b98154f4f163411\"}".getBytes())
|
||||||
|
.contentType(VndMediaType.TAG_REQUEST);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(201, response.getStatus());
|
||||||
|
assertEquals(
|
||||||
|
URI.create("/v2/repositories/space/repo/tags/newtag"),
|
||||||
|
response.getOutputHeaders().getFirst("Location"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotCreateTagIfNotPermitted() throws IOException, URISyntaxException {
|
||||||
|
doThrow(AuthorizationException.class).when(subject).checkPermission("repository:push:repoId");
|
||||||
|
Tags tags = new Tags();
|
||||||
|
tags.setTags(Lists.emptyList());
|
||||||
|
when(tagsCommandBuilder.getTags()).thenReturn(tags);
|
||||||
|
when(tagCommandBuilder.create()).thenReturn(tagCreateCommandBuilder);
|
||||||
|
when(tagCreateCommandBuilder.execute()).thenReturn(new Tag("newtag", "592d797cd36432e591416e8b2b98154f4f163411"));
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(TAG_URL)
|
||||||
|
.content("{\"name\": \"newtag\",\"revision\":\"592d797cd36432e591416e8b2b98154f4f163411\"}".getBytes())
|
||||||
|
.contentType(VndMediaType.TAG_REQUEST);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(403, response.getStatus());
|
||||||
|
verify(tagCommandBuilder, never()).create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldThrowExceptionIfTagAlreadyExists() throws URISyntaxException, IOException {
|
||||||
|
Tags tags = new Tags();
|
||||||
|
tags.setTags(Collections.singletonList(new Tag("newtag", "592d797cd36432e591416e8b2b98154f4f163411")));
|
||||||
|
when(tagsCommandBuilder.getTags()).thenReturn(tags);
|
||||||
|
when(tagCommandBuilder.create()).thenReturn(tagCreateCommandBuilder);
|
||||||
|
when(tagCreateCommandBuilder.execute()).thenReturn(new Tag("newtag", "592d797cd36432e591416e8b2b98154f4f163411"));
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.post(TAG_URL)
|
||||||
|
.content("{\"name\": \"newtag\",\"revision\":\"592d797cd36432e591416e8b2b98154f4f163411\"}".getBytes())
|
||||||
|
.contentType(VndMediaType.TAG_REQUEST);
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(409, response.getStatus());
|
||||||
|
verify(tagCommandBuilder, never()).create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldDeleteTag() throws IOException, URISyntaxException {
|
||||||
|
Tags tags = new Tags();
|
||||||
|
tags.setTags(Collections.singletonList(new Tag("newtag", "592d797cd36432e591416e8b2b98154f4f163411")));
|
||||||
|
when(tagsCommandBuilder.getTags()).thenReturn(tags);
|
||||||
|
when(tagCommandBuilder.delete()).thenReturn(tagDeleteCommandBuilder);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.delete(TAG_URL + "newtag");
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(204, response.getStatus());
|
||||||
|
verify(tagCommandBuilder).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturn204EvenIfTagDoesntExist() throws IOException, URISyntaxException {
|
||||||
|
Tags tags = new Tags();
|
||||||
|
tags.setTags(Collections.emptyList());
|
||||||
|
when(tagsCommandBuilder.getTags()).thenReturn(tags);
|
||||||
|
when(tagCommandBuilder.delete()).thenReturn(tagDeleteCommandBuilder);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.delete(TAG_URL + "newtag");
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(204, response.getStatus());
|
||||||
|
verify(tagCommandBuilder, never()).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotDeleteTagIfNotPermitted() throws IOException, URISyntaxException {
|
||||||
|
doThrow(AuthorizationException.class).when(subject).checkPermission("repository:modify:repoId");
|
||||||
|
Tags tags = new Tags();
|
||||||
|
tags.setTags(Collections.singletonList(new Tag("newtag", "592d797cd36432e591416e8b2b98154f4f163411")));
|
||||||
|
when(tagsCommandBuilder.getTags()).thenReturn(tags);
|
||||||
|
when(tagCommandBuilder.delete()).thenReturn(tagDeleteCommandBuilder);
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.delete(TAG_URL + "newtag");
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(403, response.getStatus());
|
||||||
|
verify(tagCommandBuilder, never()).delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,17 +24,29 @@
|
|||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryTestData;
|
||||||
|
import sonia.scm.repository.Signature;
|
||||||
|
import sonia.scm.repository.SignatureStatus;
|
||||||
import sonia.scm.repository.Tag;
|
import sonia.scm.repository.Tag;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class TagToTagDtoMapperTest {
|
class TagToTagDtoMapperTest {
|
||||||
@@ -45,25 +57,76 @@ class TagToTagDtoMapperTest {
|
|||||||
@InjectMocks
|
@InjectMocks
|
||||||
private TagToTagDtoMapperImpl mapper;
|
private TagToTagDtoMapperImpl mapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Subject subject;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setupSubject() {
|
||||||
|
ThreadContext.bind(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ThreadContext.unbindSubject();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAppendLinks() {
|
void shouldAppendLinks() {
|
||||||
HalEnricherRegistry registry = new HalEnricherRegistry();
|
HalEnricherRegistry registry = new HalEnricherRegistry();
|
||||||
registry.register(Tag.class, (ctx, appender) -> {
|
registry.register(Tag.class, (ctx, appender) -> {
|
||||||
NamespaceAndName repository = ctx.oneRequireByType(NamespaceAndName.class);
|
Repository repository = ctx.oneRequireByType(Repository.class);
|
||||||
Tag tag = ctx.oneRequireByType(Tag.class);
|
Tag tag = ctx.oneRequireByType(Tag.class);
|
||||||
appender.appendLink("yo", "http://" + repository.logString() + "/" + tag.getName());
|
|
||||||
|
appender.appendLink("yo", "http://" + repository.getNamespace() + "/" + repository.getName() + "/" + tag.getName());
|
||||||
});
|
});
|
||||||
mapper.setRegistry(registry);
|
mapper.setRegistry(registry);
|
||||||
|
|
||||||
TagDto dto = mapper.map(new Tag("1.0.0", "42"), new NamespaceAndName("hitchhiker", "hog"));
|
TagDto dto = mapper.map(new Tag("1.0.0", "42"), RepositoryTestData.createHeartOfGold());
|
||||||
assertThat(dto.getLinks().getLinkBy("yo").get().getHref()).isEqualTo("http://hitchhiker/hog/1.0.0");
|
assertThat(dto.getLinks().getLinkBy("yo").get().getHref()).isEqualTo("http://hitchhiker/HeartOfGold/1.0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMapDate() {
|
void shouldMapDate() {
|
||||||
final long now = Instant.now().getEpochSecond() * 1000;
|
final long now = Instant.now().getEpochSecond() * 1000;
|
||||||
TagDto dto = mapper.map(new Tag("1.0.0", "42", now), new NamespaceAndName("hitchhiker", "hog"));
|
TagDto dto = mapper.map(new Tag("1.0.0", "42", now), RepositoryTestData.createHeartOfGold());
|
||||||
assertThat(dto.getDate()).isEqualTo(Instant.ofEpochMilli(now));
|
assertThat(dto.getDate()).isEqualTo(Instant.ofEpochMilli(now));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldContainSignatureArray() {
|
||||||
|
TagDto dto = mapper.map(new Tag("1.0.0", "42"), RepositoryTestData.createHeartOfGold());
|
||||||
|
assertThat(dto.getSignatures()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldMapSignatures() {
|
||||||
|
final Tag tag = new Tag("1.0.0", "42");
|
||||||
|
tag.addSignature(new Signature("29v391239v", "gpg", SignatureStatus.VERIFIED, "me", Collections.emptySet()));
|
||||||
|
TagDto dto = mapper.map(tag, RepositoryTestData.createHeartOfGold());
|
||||||
|
assertThat(dto.getSignatures()).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAddDeleteLink() {
|
||||||
|
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||||
|
when(subject.isPermitted("repository:push:" + repository.getId())).thenReturn(true);
|
||||||
|
final Tag tag = new Tag("1.0.0", "42");
|
||||||
|
TagDto dto = mapper.map(tag, repository);
|
||||||
|
assertThat(dto.getLinks().getLinkBy("delete")).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotAddDeleteLinkIfPermissionsAreMissing() {
|
||||||
|
final Tag tag = new Tag("1.0.0", "42");
|
||||||
|
TagDto dto = mapper.map(tag, RepositoryTestData.createHeartOfGold());
|
||||||
|
assertThat(dto.getLinks().getLinkBy("delete")).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotAddDeleteLinksForUndeletableTags() {
|
||||||
|
final Tag tag = new Tag("1.0.0", "42", null, false);
|
||||||
|
TagDto dto = mapper.map(tag, RepositoryTestData.createHeartOfGold());
|
||||||
|
assertThat(dto.getLinks().getLinkBy("delete")).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||