Merge branch 'develop' into feature/filetree_file_status_icon
55
CHANGELOG.md
@@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.7.4] - 2025-03-12
|
||||
### Fixed
|
||||
- Possibility to configure 'maxFormKeys' and 'maxFormContentSize' in Jetty
|
||||
|
||||
## [3.7.3] - 2025-02-24
|
||||
### Fixed
|
||||
- Keep original timestamp on rebase
|
||||
- Title in tags (for example for "done" tags in review plugin)
|
||||
|
||||
## [3.7.1] - 2025-01-28
|
||||
### Fixed
|
||||
- Internal server error when creating tags in combination with some plugins
|
||||
|
||||
## [3.7.0] - 2025-01-23
|
||||
### Added
|
||||
- Performance improvements for git modifications
|
||||
- New button variant called "info"
|
||||
- Link to repo page in repo header
|
||||
- Extension point below the title of a repository
|
||||
- 'uploadpack.allowFilter = true' set for all new and existing Git repositories
|
||||
|
||||
### Fixed
|
||||
- Remove superfluous alt text for decorative images
|
||||
- Accessible details for contributors and tags in changesets
|
||||
- Whitespace dropdown is now correctly displayed after pr create
|
||||
|
||||
### Changed
|
||||
- Clickable tags are based on the HTML button.
|
||||
- Upgrade JGit to 7.1.0.202411261347-r
|
||||
- Set focus to first input element in repository, user, group, branch and repository role creation forms
|
||||
- Replace title behavior with `useDocumentTitle` hook for setting descriptive document titles
|
||||
|
||||
### Removed
|
||||
- Unused class `IterableQueue`
|
||||
|
||||
## [3.6.1] - 2025-01-17
|
||||
### Fixed
|
||||
- Removed the API token error log message that was being printed when the API token was invalid (fix from 2.46.5)
|
||||
|
||||
## [2.48.4] - 2025-01-17
|
||||
### Fixed
|
||||
- Fixes from version 2.46.2, 2.46.3, 2.46.4, and 2.46.5
|
||||
|
||||
## [2.46.5] - 2025-01-17
|
||||
### Fixed
|
||||
- Removed the API token error log message that was being printed when the API token was invalid
|
||||
|
||||
## [3.6.0] - 2024-12-05
|
||||
### Added
|
||||
- Extension point for contributor row in contributor table
|
||||
@@ -1636,10 +1683,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[2.46.2]: https://scm-manager.org/download/2.46.2
|
||||
[2.46.3]: https://scm-manager.org/download/2.46.3
|
||||
[2.46.4]: https://scm-manager.org/download/2.46.4
|
||||
[2.46.5]: https://scm-manager.org/download/2.46.5
|
||||
[2.47.0]: https://scm-manager.org/download/2.47.0
|
||||
[2.48.0]: https://scm-manager.org/download/2.48.0
|
||||
[2.48.1]: https://scm-manager.org/download/2.48.1
|
||||
[2.48.2]: https://scm-manager.org/download/2.48.2
|
||||
[2.48.3]: https://scm-manager.org/download/2.48.3
|
||||
[2.48.4]: https://scm-manager.org/download/2.48.4
|
||||
[3.0.0]: https://scm-manager.org/download/3.0.0
|
||||
[3.0.1]: https://scm-manager.org/download/3.0.1
|
||||
[3.0.2]: https://scm-manager.org/download/3.0.2
|
||||
@@ -1655,3 +1705,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[3.4.2]: https://scm-manager.org/download/3.4.2
|
||||
[3.5.0]: https://scm-manager.org/download/3.5.0
|
||||
[3.6.0]: https://scm-manager.org/download/3.6.0
|
||||
[3.6.1]: https://scm-manager.org/download/3.6.1
|
||||
[3.7.0]: https://scm-manager.org/download/3.7.0
|
||||
[3.7.1]: https://scm-manager.org/download/3.7.1
|
||||
[3.7.3]: https://scm-manager.org/download/3.7.3
|
||||
[3.7.4]: https://scm-manager.org/download/3.7.4
|
||||
|
||||
6
Jenkinsfile
vendored
@@ -81,9 +81,9 @@ pipeline {
|
||||
steps {
|
||||
// To rerun integration tests with each build, add '-PrerunIntegrationTests' to the gradle command
|
||||
gradle 'integrationTest'
|
||||
junit allowEmptyResults: true, testResults: 'scm-it/build/test-results/javaIntegrationTests/*.xml,scm-ui/build/reports/e2e/*.xml'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/e2e-tests/cypress/videos/*.mp4'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/e2e-tests/cypress/screenshots/**/*.png'
|
||||
junit allowEmptyResults: true, testResults: 'scm-it/build/test-results/javaIntegrationTests/*.xml,scm-ui/build/reports/e2e/*.xml,scm-ui/build/target/cypress/reports/*.xml'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/build/target/cypress/videos/*.mp4'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'scm-ui/build/target/cypress/screenshots/**/*.png'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
README.md
@@ -60,15 +60,15 @@ gradlew.bat taskname
|
||||
This following tables describes some high level tasks,
|
||||
which should cover most of the daily work.
|
||||
|
||||
| Name | Description |
|
||||
| ---- | ----------- |
|
||||
| run | Starts an SCM-Manager with enabled livereload for the ui |
|
||||
| build | Executes all checks, tests and builds the smp inclusive javadoc and source jar |
|
||||
| distribution | Builds all distribution packages of scm-packaging |
|
||||
| check | Executes all registered checks and tests |
|
||||
| test | Run all unit tests |
|
||||
| integrationTest | Run all integration tests of scm-it |
|
||||
| clean | Deletes the build directory |
|
||||
| Name | Description |
|
||||
| ---- |--------------------------------------------------------------------------------------------------------------------|
|
||||
| run | Starts an SCM-Manager with enabled livereload for the ui |
|
||||
| build | Executes all checks, tests and builds the smp inclusive [javadoc](https://scm-manager.org/javadoc/) and source jar |
|
||||
| distribution | Builds all distribution packages of scm-packaging |
|
||||
| check | Executes all registered checks and tests |
|
||||
| test | Run all unit tests |
|
||||
| integrationTest | Run all integration tests of scm-it |
|
||||
| clean | Deletes the build directory |
|
||||
|
||||
The next table defines a few more tasks which are more relevant for CI servers.
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ class JavaModulePlugin implements Plugin<Project> {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
//TODO Fix javadoc errors which breaks the build
|
||||
// withJavadocJar()
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 124 KiB |
BIN
docs/de/user/admin/assets/administration-setings-connected.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 287 KiB |
BIN
docs/de/user/admin/assets/cloudogu-platform-login.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
@@ -14,7 +14,7 @@ Einige besondere Plugins sind nur für Instanzen des SCM-Managers verfügbar, di
|
||||
|
||||

|
||||
Sie werden dann zur cloudogu platform-Login-Maske weitergeleitet.
|
||||

|
||||

|
||||
Wenn Sie über ein cloudogu platform-Konto verfügen, können Sie sich einloggen. Ansonsten erstellen Sie über einen konföderierten Identitätsanbieter (Google oder github) oder Ihre Email-Adresse ein Konto.
|
||||
Anschließend werden Sie zurück zum SCM-Manager geleitet und können Details zur verbundenen Instanz und Konto überprüfen. Mit „Verbinden“ bestätigen Sie die Verbindung, mit „Abbrechen“ brechen Sie den Vorgang ab.
|
||||

|
||||
|
||||
@@ -24,13 +24,26 @@ Auf der Login-Seite des SCM-Managers werden hilfreiche Plugins und Features vorg
|
||||
Um Angriffe auf den SCM-Manager mit Cross Site Scripting (XSS / XSRF) zu erschweren. Dieses Feature ist noch experimentell.
|
||||
|
||||
#### Plugin-Settings
|
||||
Der SCM-Manager kann ein Plugin-Center anbinden, um schnell und bequem Plugins verwalten zu können. Um ein anderes SCM-Plugin-Center als das vorkonfigurierte zu verwenden, reicht es aus diese URL zu ändern. Läuft der SCM-Manager im Cloudogu EcoSystem kann die Plugin Center URL über einen Eintrag im etcd gesetzt werden.
|
||||
Der SCM-Manager kann ein Plugin-Center anbinden, um schnell und bequem Plugins verwalten zu können. Um ein anderes SCM-Plugin-Center als das vorkonfigurierte zu verwenden, reicht es aus diese URL zu ändern. Läuft der SCM-Manager im Cloudogu EcoSystem, kann die Plugin-Center URL über einen Eintrag im etcd gesetzt werden.
|
||||
Wenn das vorkonfigurierte Plugin-Center verwendet wird, kann der SCM-Manager mit der cloudogu platform verbunden werden.
|
||||
|
||||
Nach der initialen Einrichtung sind folgende Werte standardgemäß hinterlegt:
|
||||
```markdown
|
||||
Plugin Center URL: https://plugin-center-api.scm-manager.org/api/v1/plugins/{version}?os={os}&arch={arch}&jre={jre}
|
||||
Plugin Center Authentication URL: https://plugin-center-api.scm-manager.org/api/v1/auth/oidc
|
||||
```
|
||||
|
||||

|
||||
So können über das Plugin-Center besondere cloudogu platform-Plugins bezogen werden. Details sind in der Dokumentation des Plugin-Centers aufgeführt.
|
||||
Eine bestehende Verbindung zwischen dem SCM-Manager und der cloudogu platform kann hier aufgehoben werden.
|
||||

|
||||
|
||||
### JWT Einstellungen
|
||||
Benutzer erhalten einen JWT als Authentifizierungstoken nach einem erfolgreichen login.
|
||||
Administratoren können die Lebensdauer dieser JWTs konfigurieren.
|
||||
Falls die Lebensdauer verringert wird, wird jeder bisher ausgestellter JWT ungültig.
|
||||
Sollte in der `config.yml` des Servers die Option "endless JWT" aktiviert sein, dann wird diese Einstellung ignoriert.
|
||||
|
||||
#### Anonyme Zugriff
|
||||
Der SCM-Manager 2 hat das Konzept für anonyme Zugriffe über einen "_anonymous"-Benutzer realisiert. Beim Aktivieren des anonymen Zugriffs wird ein neuer Benutzer erstellt mit dem Namen "_anonymous". Dieser Nutzer kann wie ein gewöhnlicher Benutzer für unterschiedliche Aktionen berechtigt werden. Bei einem Zugriff auf den SCM-Manager ohne Zugangsdaten wird dieser anonyme Benutzer verwendet.
|
||||
Ist der anonyme Zugriff nur für Protokoll aktiviert, können die REST API und die VCS Protokolle anonym genutzt werden. Wurde der anonyme Zugriff vollständig aktiviert, ist auch ein Zugriff über den Webclient anonym möglich.
|
||||
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 150 KiB |
BIN
docs/de/user/repo/assets/repository-code-changeset-revert.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
@@ -84,6 +84,24 @@ Es muss lediglich ein gewünschter Name angegeben werden, welcher die gleichen F
|
||||
|
||||

|
||||
|
||||
#### Reverts
|
||||
In Changesets innerhalb von Git-Repositories steht in der oberen rechten Ecke (unter "Tag erstellen") ein Knopf zum Reverten des Commits.
|
||||
|
||||
**Hinweis:** Der Revert-Knopf wird nur dann angezeigt, wenn der Commit genau einen Vorgänger hat.
|
||||
Commits mit mehr als einem Vorgänger (z.B. Merge-Commits) und initiale Commits ohne Vorgänger können nicht zurückgesetzt werden.
|
||||
|
||||

|
||||
|
||||
Für einen Revert ist nach Drücken des Knopfs ein Branch auszuwählen, auf welchem der Revert angewendet wird.
|
||||
Gelangt man aus der Commit-Übersicht eines Branches in den Commit, ist die Auswahl automatisch vorgenommen.
|
||||
|
||||
Ebenso kann eine Commit-Nachricht für den Revert angegeben werden.
|
||||
Sie ist automatisch ausgefüllt; es empfiehlt sich jedoch aus Gründen der Übersichtlichkeit, in dieser den Revert zu begründen.
|
||||
|
||||
Mit Drücken von "Revert" wird man auf den neu erstellten Revert-Commit automatisch weitergeleitet, sofern kein Fehler auftritt.
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ verfügbaren Tastenkürzel mittels der `?`-Taste aufrufen.
|
||||
|
||||
### Globale Tastenkürzel
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|-------------------------------------|
|
||||
| ? | Öffne die Tastaturkürzelübersicht |
|
||||
| / | Fokussiere die globale Schnellsuche |
|
||||
| alt r | Navigiere zur Repositoryübersicht |
|
||||
| alt u | Navigiere zur Benutzerübersicht |
|
||||
| alt g | Navigiere zur Gruppenübersicht |
|
||||
| alt a | Navigiere zur Administration |
|
||||
| Tastenkürzel | Beschreibung |
|
||||
|--------------|-------------------------------------|
|
||||
| ? | Öffne die Tastaturkürzelübersicht |
|
||||
| / | Fokussiere die globale Schnellsuche |
|
||||
| alt r | Navigiere zur Repositoryübersicht |
|
||||
| alt u | Navigiere zur Benutzerübersicht |
|
||||
| alt g | Navigiere zur Gruppenübersicht |
|
||||
| alt a | Navigiere zur Administration |
|
||||
|
||||
### Navigation von Listen
|
||||
|
||||
@@ -25,20 +25,25 @@ Einige Seiten mit Listen erlauben die Navigation per Tastatur.
|
||||
Wenn die Seite dieses unterstützt, tauchen die Tastaturkürzel in der Übersicht im SCM-Manager
|
||||
auf (`?`).
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|---------------------------------------------------|
|
||||
| j | Bewege den Fokus auf den nächsten Listeneintrag |
|
||||
| k | Bewege den Fokus auf den vorherigen Listeneintrag |
|
||||
| Tastenkürzel | Beschreibung |
|
||||
|--------------|---------------------------------------------------|
|
||||
| j | Bewege den Fokus auf den nächsten Listeneintrag |
|
||||
| k | Bewege den Fokus auf den vorherigen Listeneintrag |
|
||||
|
||||
### Repositoryspezifische Tastenkürzel
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|------------------------------|
|
||||
| g i | Wechsel zur Repository-Info |
|
||||
| g b | Wechsel zu den Branches |
|
||||
| g t | Wechsel zu den Tags |
|
||||
| g c | Wechsel zum Code |
|
||||
| g s | Wechsel zu den Einstellungen |
|
||||
| Tastenkürzel | Beschreibung |
|
||||
|--------------|------------------------------|
|
||||
| g i | Wechsel zur Repository-Info |
|
||||
| g b | Wechsel zu den Branches |
|
||||
| g t | Wechsel zu den Tags |
|
||||
| g c | Wechsel zum Code |
|
||||
| g s | Wechsel zu den Einstellungen |
|
||||
|
||||
### Codespezifische Tastenkürzel
|
||||
| Tastenkürzel | Beschreibung |
|
||||
|--------------|------------------------|
|
||||
| g f | Wechsel zur Dateisuche |
|
||||
|
||||
### Tastenkürzel aus Plugin
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
title: JWT Configuration
|
||||
---
|
||||
|
||||
SCM-Manager uses [JWT](https://datatracker.ietf.org/doc/html/rfc7519) to authenticate its users.
|
||||
The creation of JWTs can be controlled via Java system properties.
|
||||
|
||||
## Endless JWT
|
||||
|
||||
Usually a JWT contains the exp claim. This claim determines how long a JWT is valid by defining an expiration time.
|
||||
If the JWT does not contain this claim, then the JWT is valid forever until the secret for the signature changes.
|
||||
Per default the JWT created by the SCM-Manager contain the exp claim with a duration of one hour.
|
||||
|
||||
If needed, it is possible to configure the SCM-Manager, so that the JWT get created without the exp claim.
|
||||
Therefore, the user session would be endless.
|
||||
|
||||
We advise **against** this behavior, because limited lifespans for JWT improve security.
|
||||
But if you really need it, you can enable endless JWT by starting the SCM-Manager with this flag:
|
||||
|
||||
```
|
||||
-Dscm.endlessJwt="true"
|
||||
```
|
||||
|
||||
If you want to disable the feature, then restart the SCM-Manager without this flag.
|
||||
If you want to invalidate already created endless JWT, then restarting the SCM-Manager, with the endless JWT feature disabled, is enough.
|
||||
The SCM-Manager will automatically create new secrets for the JWT and therefore invalidate every already existing JWT.
|
||||
5
docs/en/development/javadoc.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Javadoc
|
||||
---
|
||||
|
||||
You can find the Javadoc at [javadoc](https://scm-manager.org/javadoc/).
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Style Guide
|
||||
title: Styleguide
|
||||
---
|
||||
|
||||
Starting with version 2 of SCM-Manager we have decided to change the code style and conform to more common rules. Furthermore we abandon the rule, that everything needs to have a javadoc description. Nonetheless we have decided against a "big bang" adaption of the new rule, because this would have lead to enourmous problems for merges from 1.x to 2.x.
|
||||
@@ -107,4 +107,4 @@ more skill. If you feel overwhelmed by them, do not let them deter you. We love
|
||||
|
||||
## JavaScript
|
||||
|
||||
Take a look at our styleguide using `yarn serve` in [ui-styles](scm-ui/ui-styles) directory.
|
||||
Take a look at our styleguide using `yarn serve` in `/scm-ui/ui-styles` directory.
|
||||
@@ -53,25 +53,20 @@ If however you have to install plugins manually (for example because you cannot
|
||||
|
||||
# Huge number of repositories
|
||||
|
||||
If you have more than 100 Repositories to migrate, you may have to adapt some configuration and increase the limit of jetty form keys. You can do this by setting the `maxFormKeys` and `maxFormContentSize` of the webapp in `conf/server-config.xml`. You have to add the keys to the `WebAppContext` with the id `"scm-webapp"` e.g.:
|
||||
If you have more than 100 Repositories to migrate, you may have to adapt some configuration and increase the limit of jetty form keys. You can do this by setting the `maxFormKeys` and `maxFormContentSize` in your `conf/config.yml` file. You have to add the keys at top level of the yaml file:
|
||||
|
||||
```
|
||||
<New id="scm-webapp" class="org.eclipse.jetty.webapp.WebAppContext">
|
||||
<Set name="contextPath">/scm</Set>
|
||||
<Set name="war">
|
||||
<SystemProperty name="basedir" default="."/>/var/webapp/scm-webapp.war</Set>
|
||||
<!-- disable directory listings -->
|
||||
<Call name="setInitParameter">
|
||||
<Arg>org.eclipse.jetty.servlet.Default.dirAllowed</Arg>
|
||||
<Arg>false</Arg>
|
||||
</Call>
|
||||
<Set name="tempDirectory">
|
||||
<SystemProperty name="basedir" default="."/>/work/scm
|
||||
</Set>
|
||||
<!-- Set max form keys -->
|
||||
<Set name="maxFormContentSize">1000000</Set>
|
||||
<Set name="maxFormKeys">5000</Set>
|
||||
</New>
|
||||
# base server config
|
||||
## Address to listen 0.0.0.0 means on every interface
|
||||
addressBinding: 0.0.0.0
|
||||
port: 8080
|
||||
contextPath: /scm
|
||||
|
||||
## Additions for the huge number of repositories:
|
||||
maxFormContentSize: 1000000
|
||||
maxFormKeys: 5000
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The value for `maxFormKeys` should be the count of your repositories * 3 + 10. The `maxFormContentSize` depends on the length of your repository namespace and name, but you should be safe with repository count * 100.
|
||||
|
||||
@@ -31,16 +31,21 @@
|
||||
entries:
|
||||
- /development/intellij-idea-configuration/
|
||||
- /development/build-from-source/
|
||||
- /development/architecture-overview/
|
||||
- /development/ui-common-pitfall/
|
||||
- /development/permission-concept/
|
||||
- /development/error-handling/
|
||||
- /development/i18n/
|
||||
- /development/definition-of-done/
|
||||
- /development/ui-dod/
|
||||
- /development/decision-table/
|
||||
- /development/building-forms/
|
||||
- /development/error-handling/
|
||||
- /development/styleguide/
|
||||
- /development/testing-guide/
|
||||
- /development/integration-tests/
|
||||
- /development/ui-extensions/
|
||||
- /development/i18n/
|
||||
- /development/building-forms/
|
||||
- /development/javadoc/
|
||||
- /development/cli-guideline/
|
||||
- /development/definition-of-done/
|
||||
- /development/ui-dod/
|
||||
|
||||
- section: Plugin Development
|
||||
entries:
|
||||
|
||||
@@ -109,7 +109,7 @@ Then apply your fixes (eg. by cherry picking the relevant commits) and update th
|
||||
have single changelog yaml files, you could use the `updateChangelog` like above). Add the `CHANGELOG.md`,
|
||||
remove the yamls, and push the hotfix branch:
|
||||
|
||||
```
|
||||
```bash
|
||||
git rm -rf gradle/changelog
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Adjust changelog for release 2.30.1"
|
||||
@@ -125,7 +125,7 @@ Depending on whether you released a hotfix for an older version or the latest re
|
||||
the `main` branch to the new tag. So in our example, if there is no version `2.31.x` yet, the new version
|
||||
`2.30.1` is the latest version and we have to update `main`:
|
||||
|
||||
```
|
||||
```bash
|
||||
git checkout main
|
||||
git merge 2.30.1
|
||||
git push origin main
|
||||
@@ -136,7 +136,7 @@ that there are no changes that all changes are part of the current `develop` sta
|
||||
to merge conflicts, because the version on `develop` has been set to a new `SNAPSHOT`, while the version
|
||||
of the hotfix has been updated to the new release version. So you have to merge all these conflicts manually.
|
||||
|
||||
```
|
||||
```bash
|
||||
git checkout develop
|
||||
git merge main
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 124 KiB |
BIN
docs/en/user/admin/assets/administration-setings-connected.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 108 KiB |
@@ -26,10 +26,23 @@ Activate this option to make attacks using cross site scripting (XSS / XSRF) on
|
||||
#### Plugin-Settings
|
||||
A plugin center can be used to conveniently manage plugins. If you want to use a plugin center that is not the default one, you only have to change this URL. If SCM-Manager is operated as part of a Cloudogu EcoSystem, the plugin center URL can be changed in the etcd.
|
||||
If the default plugin center is used, the SCM-Manager may be connected to the cloudogu platform to receive special cloudogu platform-Plugins. Details can be found in the plugin-center documentation.
|
||||
|
||||
After the initial setup, the following values are set by default:
|
||||
```markdown
|
||||
Plugin Center URL: https://plugin-center-api.scm-manager.org/api/v1/plugins/{version}?os={os}&arch={arch}&jre={jre}
|
||||
Plugin Center Authentication URL: https://plugin-center-api.scm-manager.org/api/v1/auth/oidc
|
||||
```
|
||||
|
||||

|
||||
An existing connection between a SCM-Manager and the cloudogu platform may be severed here.
|
||||

|
||||
|
||||
#### JWT settings
|
||||
Users receive a JWT as an authentication token, after a successful login.
|
||||
Administrators can configure the amount of hours until a JWT expires.
|
||||
If the amount of hours get reduced, each created JWT will be invalidated.
|
||||
This setting will be ignored, if the endless JWT option is set to true in the server `config.yml`.
|
||||
|
||||
#### Anonymous Access
|
||||
In SCM-Manager 2 the access for anonymous access is realized by using an "_anonymous" user. When the feature is activated, a new user with the name "_anonymous" is created. This user can be authorized just like any other user. This user is used for access to SCM-Manager without login credentials.
|
||||
If the anonymous mode is protocol only you may access the SCM-Manager via the REST API and VCS protocols. With fully enabled anonymous access you can also use the webclient without credentials.
|
||||
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 140 KiB |
BIN
docs/en/user/repo/assets/repository-code-changeset-revert.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
@@ -85,6 +85,24 @@ Only a name has to be provided that meets the same formatting conditions as bran
|
||||
|
||||

|
||||
|
||||
#### Reverts
|
||||
Changesets within Git repositories provide a "Revert" button at the upper right-hand corner (beneath "Create Tag").
|
||||
|
||||
**Note:** The revert button is only displayed on commits with exactly one parent element.
|
||||
Commits with multiple predecessors (e.g. merge commits) and initial commits without parent cannot be reverted.
|
||||
|
||||

|
||||
|
||||
After pressing the button and before reverting a changeset, you need to first select the branch where it is applied upon.
|
||||
This selection is already filled out if you reached the changeset by the changeset overview of a specific branch.
|
||||
|
||||
Furthermore, you may type a commit message for the revert.
|
||||
It is filled out with a default message; however, it is recommended to choose a reasonable custom message in order to keep the changeset history comprehensible.
|
||||
|
||||
By pressing "Revert", you're going to be forwarded to the newly created commit including the revert if no error occurs.
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@ If the page supports this feature, the shortcuts show up in the shortcut overvie
|
||||
| g c | Switch to code |
|
||||
| g s | Switch to settings |
|
||||
|
||||
### Code-specific Shortcuts
|
||||
|
||||
| Key Combination | Description |
|
||||
|-----------------|---------------------------|
|
||||
| g f | Switch to file search |
|
||||
|
||||
### Plugin Shortcuts
|
||||
|
||||
Plugins can introduce new shortcuts.
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
#
|
||||
|
||||
group = sonia.scm
|
||||
version = 3.6.1-SNAPSHOT
|
||||
version = 3.7.5-SNAPSHOT
|
||||
org.gradle.jvmargs=-Xmx1024M
|
||||
org.gradle.caching=true
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: fixed
|
||||
description: Accessible details for contributors and tags in changesets
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: changed
|
||||
description: Clickable tags are based on the HTML button.
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: added
|
||||
description: Link to repo page in repo header
|
||||
2
gradle/changelog/adjust_file_diff_tree_height.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Entries are shown correctly
|
||||
2
gradle/changelog/alert_styling.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Incorrect alert message styling for long version conditions
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: changed
|
||||
description: Set focus to first input element in repository, user, group, branch and repository role creation forms
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: changed
|
||||
description: Replace title behavior with `useDocumentTitle` hook for setting descriptive document titles
|
||||
4
gradle/changelog/file_search.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
- type: added
|
||||
description: Keyboard shortcut (g+f) within code view for file search
|
||||
- type: changed
|
||||
description: File search page design; in particular with regard to accessibility
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: fixed
|
||||
description: Whitespace dropdown is now correctly displayed after pr create
|
||||
2
gradle/changelog/git_revert.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Git revert commit functionality
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: removed
|
||||
description: Unused class `IterableQueue`
|
||||
2
gradle/changelog/jwt_settings.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: JWT expiration time in general settings
|
||||
@@ -1,2 +0,0 @@
|
||||
- type: fixed
|
||||
description: Remove superfluous alt text for decorative images
|
||||
2
gradle/changelog/repo_type.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Selection of undefined type in import repository dialog
|
||||
10
gradle/changelog/repository-type-config-forms.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
- type: added
|
||||
description: Support of description text for input fields and checkboxes
|
||||
- type: added
|
||||
description: aria-describedby for input fields and checkboxes
|
||||
- type: changed
|
||||
description: Disabling git repositories is now the first setting within the configuration form
|
||||
- type: changed
|
||||
description: Disabling hg repositories is now the first setting within the configuration form
|
||||
- type: changed
|
||||
description: Disabling svn repositories is now the first setting within the configuration form
|
||||
2
gradle/changelog/secondary_navigation_context.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Loop in secondary navigation render cycle
|
||||
2
gradle/changelog/tertiary_buttons.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Usability and accessibility of tertiary button
|
||||
@@ -13,16 +13,16 @@ ext {
|
||||
sspVersion = '1.3.0'
|
||||
jjwtVersion = '0.11.5'
|
||||
bouncycastleVersion = '2.73.6'
|
||||
jettyVersion = '11.0.16'
|
||||
jettyVersion = '11.0.24'
|
||||
luceneVersion = '8.11.4'
|
||||
|
||||
junitJupiterVersion = '5.10.3'
|
||||
hamcrestVersion = '3.0'
|
||||
mockitoVersion = '5.14.2'
|
||||
jerseyVersion = '3.1.3'
|
||||
jerseyVersion = '3.1.9'
|
||||
micrometerVersion = '1.14.2'
|
||||
|
||||
nodeVersion = '21.6.2'
|
||||
nodeVersion = '21.7.3'
|
||||
yarnVersion = '1.22.18'
|
||||
|
||||
libraries = [
|
||||
@@ -73,7 +73,7 @@ ext {
|
||||
edison: 'de.otto.edison:edison-hal:2.1.1',
|
||||
|
||||
// openapi
|
||||
swaggerJaxRs: 'io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.19',
|
||||
swaggerJaxRs: 'io.swagger.core.v3:swagger-jaxrs2-jakarta:2.2.26',
|
||||
|
||||
// dto mapping
|
||||
mapstruct: "org.mapstruct:mapstruct:${mapstructVersion}",
|
||||
@@ -96,8 +96,8 @@ ext {
|
||||
// utils
|
||||
guava: 'com.google.guava:guava:33.3.1-jre',
|
||||
commonsLang: 'commons-lang:commons-lang:2.6',
|
||||
commonsCompress: 'org.apache.commons:commons-compress:1.23.0',
|
||||
commonsIo: 'commons-io:commons-io:2.13.0',
|
||||
commonsCompress: 'org.apache.commons:commons-compress:1.27.1',
|
||||
commonsIo: 'commons-io:commons-io:2.18.0',
|
||||
commonsLang3: 'org.apache.commons:commons-lang3:3.13.0',
|
||||
|
||||
// security
|
||||
|
||||
46
scm-core/src/main/java/sonia/scm/ConflictException.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
public class ConflictException extends ExceptionWithContext {
|
||||
private static final String CODE = "7XUd94Iwo1";
|
||||
|
||||
public ConflictException(NamespaceAndName namespaceAndName, Collection<String> conflictingFiles) {
|
||||
super(
|
||||
createContext(namespaceAndName, conflictingFiles),
|
||||
"conflict"
|
||||
);
|
||||
}
|
||||
|
||||
private static List<ContextEntry> createContext(NamespaceAndName namespaceAndName, Collection<String> conflictingFiles) {
|
||||
return entity("files", String.join(", ", conflictingFiles))
|
||||
.in(namespaceAndName)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,22 @@ public class ScmConfiguration implements Configuration {
|
||||
@XmlElement(name = "mail-domain-name")
|
||||
private String mailDomainName = DEFAULT_MAIL_DOMAIN_NAME;
|
||||
|
||||
/**
|
||||
* Time in hours for jwt expiration.
|
||||
*
|
||||
* @since 3.8.0
|
||||
*/
|
||||
@XmlElement(name = "jwt-expiration-time")
|
||||
private int jwtExpirationInH = 1;
|
||||
|
||||
/**
|
||||
* Enables endless jwt.
|
||||
*
|
||||
* @since 3.8.0
|
||||
*/
|
||||
@XmlElement(name = "jwt-expiration-endless")
|
||||
private boolean enabledJwtEndless = false;
|
||||
|
||||
/**
|
||||
* List of users that will be notified of administrative incidents.
|
||||
*
|
||||
@@ -278,6 +294,8 @@ public class ScmConfiguration implements Configuration {
|
||||
this.emergencyContacts = other.emergencyContacts;
|
||||
this.enabledUserConverter = other.enabledUserConverter;
|
||||
this.enabledApiKeys = other.enabledApiKeys;
|
||||
this.jwtExpirationInH = other.jwtExpirationInH;
|
||||
this.enabledJwtEndless = other.enabledJwtEndless;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,6 +466,26 @@ public class ScmConfiguration implements Configuration {
|
||||
return anonymousMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Jwt expiration in {@code n} .
|
||||
*
|
||||
* @return Jwt expiration in {@code number}
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public int getJwtExpirationInH() {
|
||||
return jwtExpirationInH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the cookie xsrf protection is enabled.
|
||||
*
|
||||
* @return {@code true} if the cookie xsrf protection is enabled
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public boolean isJwtEndless() {
|
||||
return enabledJwtEndless;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if anonymous mode is enabled.
|
||||
*
|
||||
@@ -728,6 +766,26 @@ public class ScmConfiguration implements Configuration {
|
||||
this.enabledFileSearch = enabledFileSearch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@code n} to configure jwt expiration time in hours
|
||||
*
|
||||
* @param jwtExpirationInH {@code n} to configure jwt expiration time in hours
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public void setJwtExpirationInH(int jwtExpirationInH) {
|
||||
this.jwtExpirationInH = jwtExpirationInH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@code true} to enable endless jwt.
|
||||
*
|
||||
* @param enabledJwtEndless {@code true} to enable endless jwt.
|
||||
* @since 2.45.0
|
||||
*/
|
||||
public void setEnabledJwtExpiration(boolean enabledJwtEndless) {
|
||||
this.enabledJwtEndless = enabledJwtEndless;
|
||||
}
|
||||
|
||||
public void setNamespaceStrategy(String namespaceStrategy) {
|
||||
this.namespaceStrategy = namespaceStrategy;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.BadRequestException;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
|
||||
public class MultipleParentsNotAllowedException extends BadRequestException {
|
||||
public MultipleParentsNotAllowedException(String changeset) {
|
||||
super(
|
||||
Collections.emptyList(),
|
||||
String.format("%s has more than one parent changeset, which is not allowed with this request.", changeset));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "3a47Hzu1e3";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import lombok.Getter;
|
||||
import sonia.scm.BadRequestException;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
/**
|
||||
* Thrown when a changeset has no parent.
|
||||
* @since 3.8
|
||||
*/
|
||||
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
|
||||
@Getter
|
||||
public class NoParentException extends BadRequestException {
|
||||
|
||||
public NoParentException(String changeset) {
|
||||
super(emptyList(), String.format("%s has no parent.", changeset));
|
||||
this.revision = changeset;
|
||||
}
|
||||
|
||||
private final String revision;
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "a37jI66dup";
|
||||
}
|
||||
}
|
||||
@@ -82,5 +82,10 @@ public enum Command
|
||||
/**
|
||||
* @since 2.39.0
|
||||
*/
|
||||
CHANGESETS
|
||||
CHANGESETS,
|
||||
|
||||
/**
|
||||
* @since 3.8
|
||||
*/
|
||||
REVERT
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
@@ -55,22 +56,6 @@ import java.util.stream.Stream;
|
||||
* after work is finished. For closing the connection to the repository use the
|
||||
* {@link #close()} method.
|
||||
*
|
||||
* @apiviz.uses sonia.scm.repository.Feature
|
||||
* @apiviz.uses sonia.scm.repository.api.Command
|
||||
* @apiviz.uses sonia.scm.repository.api.BlameCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.BrowseCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.CatCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.DiffCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.LogCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.TagsCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.BranchesCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.IncomingCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.OutgoingCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.PullCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.PushCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
|
||||
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
|
||||
* @since 1.17
|
||||
*/
|
||||
public final class RepositoryService implements Closeable {
|
||||
@@ -80,7 +65,10 @@ public final class RepositoryService implements Closeable {
|
||||
private final CacheManager cacheManager;
|
||||
private final PreProcessorUtil preProcessorUtil;
|
||||
private final RepositoryServiceProvider provider;
|
||||
|
||||
@Getter
|
||||
private final Repository repository;
|
||||
|
||||
@SuppressWarnings({"rawtypes", "java:S3740"})
|
||||
private final Set<ScmProtocolProvider> protocolProviders;
|
||||
private final WorkdirProvider workdirProvider;
|
||||
@@ -119,7 +107,7 @@ public final class RepositoryService implements Closeable {
|
||||
|
||||
/**
|
||||
* Closes the connection to the repository and releases all locks
|
||||
* and resources. This method should be called in a finally block e.g.:
|
||||
* and resources. This method should be called in a finally block; e.g.:
|
||||
*
|
||||
* <pre><code>
|
||||
* RepositoryService service = null;
|
||||
@@ -143,7 +131,29 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The blame command shows changeset information by line for a given file.
|
||||
* Returns true if the command is supported by the repository service.
|
||||
*
|
||||
* @param command command
|
||||
* @return true if the command is supported
|
||||
*/
|
||||
public boolean isSupported(Command command) {
|
||||
return provider.getSupportedCommands().contains(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the feature is supported by the repository service.
|
||||
*
|
||||
* @param feature feature
|
||||
* @return true if the feature is supported
|
||||
* @since 1.25
|
||||
*/
|
||||
public boolean isSupported(Feature feature) {
|
||||
return provider.getSupportedFeatures().contains(feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BlameCommandBuilder}. It can take the respective parameters and be executed to show
|
||||
* changeset information by line for a given file.
|
||||
*
|
||||
* @return instance of {@link BlameCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -157,21 +167,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The branches command list all repository branches.
|
||||
*
|
||||
* @return instance of {@link BranchesCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public BranchesCommandBuilder getBranchesCommand() {
|
||||
LOG.debug("create branches command for repository {}", repository);
|
||||
|
||||
return new BranchesCommandBuilder(cacheManager,
|
||||
provider.getBranchesCommand(), repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* The branch command creates new branches.
|
||||
* Creates a {@link BranchCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* create new branches, if supported by the particular SCM system.
|
||||
*
|
||||
* @return instance of {@link BranchCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -186,7 +183,37 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The browse command allows browsing of a repository.
|
||||
* Creates a {@link BranchDetailsCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* get details for a branch.
|
||||
*
|
||||
* @return instance of {@link BranchDetailsCommand}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.28.0
|
||||
*/
|
||||
public BranchDetailsCommandBuilder getBranchDetailsCommand() {
|
||||
LOG.debug("create branch details command for repository {}", repository);
|
||||
return new BranchDetailsCommandBuilder(repository, provider.getBranchDetailsCommand(), cacheManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BranchesCommandBuilder}. It can take the respective parameters and be executed to list
|
||||
* all repository branches.
|
||||
*
|
||||
* @return instance of {@link BranchesCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
*/
|
||||
public BranchesCommandBuilder getBranchesCommand() {
|
||||
LOG.debug("create branches command for repository {}", repository);
|
||||
|
||||
return new BranchesCommandBuilder(cacheManager,
|
||||
provider.getBranchesCommand(), repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BrowseCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* browse for content within a repository.
|
||||
*
|
||||
* @return instance of {@link BrowseCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -200,7 +227,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundle command creates an archive from the repository.
|
||||
* Creates a {@link BundleCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* create an archive from the repository.
|
||||
*
|
||||
* @return instance of {@link BundleCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -214,7 +242,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The cat command show the content of a given file.
|
||||
* Creates a {@link CatCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* show the content of a given file.
|
||||
*
|
||||
* @return instance of {@link CatCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -227,8 +256,21 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The diff command shows differences between revisions for a specified file
|
||||
* or the entire revision.
|
||||
* Creates a {@link ChangesetsCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* retrieve a set of at least one changeset.
|
||||
*
|
||||
* @return Instance of {@link ChangesetsCommandBuilder}.
|
||||
* @throws CommandNotSupportedException if the command is not supported by
|
||||
* the implementation of the {@link RepositoryServiceProvider}.
|
||||
*/
|
||||
public ChangesetsCommandBuilder getChangesetsCommand() {
|
||||
LOG.debug("create changesets command for repository {}", repository);
|
||||
return new ChangesetsCommandBuilder(repository, provider.getChangesetsCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link DiffCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* show differences between revisions for a specified file or the entire revision.
|
||||
*
|
||||
* @return instance of {@link DiffCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -241,8 +283,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The diff command shows differences between revisions for a specified file
|
||||
* or the entire revision.
|
||||
* Creates a {@link DiffResultCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* show differences between revisions for a specified file or the entire revision.
|
||||
*
|
||||
* @return instance of {@link DiffResultCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -255,8 +297,36 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The incoming command shows new {@link Changeset}s found in a different
|
||||
* repository location.
|
||||
* Creates a {@link FullHealthCheckCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* inspect a repository profoundly. This might take a while in contrast to the lighter checks executed at startup.
|
||||
*
|
||||
* @return instance of {@link FullHealthCheckCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public FullHealthCheckCommandBuilder getFullCheckCommand() {
|
||||
LOG.debug("create full check command for repository {}", repository);
|
||||
return new FullHealthCheckCommandBuilder(provider.getFullHealthCheckCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FileLockCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* lock and unlock files.
|
||||
*
|
||||
* @return instance of {@link FileLockCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.26.0
|
||||
*/
|
||||
public FileLockCommandBuilder getLockCommand() {
|
||||
LOG.debug("create lock command for repository {}", repository);
|
||||
return new FileLockCommandBuilder(provider.getFileLockCommand(), repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link IncomingCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* show new {@link Changeset}s found in a different repository location.
|
||||
*
|
||||
* @return instance of {@link IncomingCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -271,7 +341,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The log command shows revision history of entire repository or files.
|
||||
* Creates a {@link LogCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* show revision history of entire repository or files.
|
||||
*
|
||||
* @return instance of {@link LogCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -285,7 +356,54 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The modification command shows file modifications in a revision.
|
||||
* Creates a {@link LookupCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* conduct a lookup which returns additional information for the repository.
|
||||
*
|
||||
* @return instance of {@link LookupCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public LookupCommandBuilder getLookupCommand() {
|
||||
LOG.debug("create lookup command for repository {}", repository);
|
||||
return new LookupCommandBuilder(provider.getLookupCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MergeCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* conduct a merge of two branches.
|
||||
*
|
||||
* @return instance of {@link MergeCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public MergeCommandBuilder getMergeCommand() {
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
LOG.debug("create merge command for repository {}", repository);
|
||||
|
||||
return new MergeCommandBuilder(provider.getMergeCommand(), eMail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MirrorCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* create a 'mirror' of an existing repository (specified by a URL) by copying all data
|
||||
* to the repository of this service. Therefore, this repository has to be empty (otherwise the behaviour is
|
||||
* not specified).
|
||||
*
|
||||
* @return instance of {@link MirrorCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.19.0
|
||||
*/
|
||||
public MirrorCommandBuilder getMirrorCommand() {
|
||||
LOG.debug("create mirror command for repository {}", repository);
|
||||
return new MirrorCommandBuilder(provider.getMirrorCommand(), repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ModificationsCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* show file modifications in a revision.
|
||||
*
|
||||
* @return instance of {@link ModificationsCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -297,7 +415,25 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The outgoing command show {@link Changeset}s not found in a remote repository.
|
||||
* Creates a {@link ModifyCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* makes changes to the files within a changeset.
|
||||
*
|
||||
* @return instance of {@link ModifyCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @see ModifyCommandBuilder
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public ModifyCommandBuilder getModifyCommand() {
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
LOG.debug("create modify command for repository {}", repository);
|
||||
|
||||
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, repository.getId(), eMail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link OutgoingCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* show {@link Changeset}s not found in a remote repository.
|
||||
*
|
||||
* @return instance of {@link OutgoingCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -312,7 +448,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The pull command pull changes from a other repository.
|
||||
* Creates a {@link PullCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* pull changes from another repository.
|
||||
*
|
||||
* @return instance of {@link PullCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -327,7 +464,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The push command pushes changes to a other repository.
|
||||
* Creates a {@link PushCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* push changes to another repository.
|
||||
*
|
||||
* @return instance of {@link PushCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -341,12 +479,18 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the repository of this service.
|
||||
* Creates a {@link RevertCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* apply a revert of a chosen changeset onto the given repository/branch combination.
|
||||
*
|
||||
* @return repository of this service
|
||||
* @return Instance of {@link RevertCommandBuilder}.
|
||||
* @throws CommandNotSupportedException if the command is not supported by
|
||||
* the implementation of the {@link RepositoryServiceProvider}.
|
||||
* @since 3.8
|
||||
* @see RevertCommandBuilder
|
||||
*/
|
||||
public Repository getRepository() {
|
||||
return repository;
|
||||
public RevertCommandBuilder getRevertCommand() {
|
||||
LOG.debug("create revert command for repository {}", repository);
|
||||
return new RevertCommandBuilder(provider.getRevertCommand(), eMail);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,7 +520,8 @@ public final class RepositoryService implements Closeable {
|
||||
}
|
||||
|
||||
/**
|
||||
* The unbundle command restores a repository from the given bundle.
|
||||
* Creates an {@link UnbundleCommandBuilder}. It can take the respective parameters and be executed to
|
||||
* restore a repository from the given bundle.
|
||||
*
|
||||
* @return instance of {@link UnbundleCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
@@ -390,137 +535,6 @@ public final class RepositoryService implements Closeable {
|
||||
repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* The merge command executes a merge of two branches. It is possible to do a dry run to check, whether the given
|
||||
* branches can be merged without conflicts.
|
||||
*
|
||||
* @return instance of {@link MergeCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public MergeCommandBuilder getMergeCommand() {
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
LOG.debug("create merge command for repository {}", repository);
|
||||
|
||||
return new MergeCommandBuilder(provider.getMergeCommand(), eMail);
|
||||
}
|
||||
|
||||
/**
|
||||
* The modify command makes changes to the head of a branch. It is possible to
|
||||
* <ul>
|
||||
* <li>create new files</li>
|
||||
* <li>delete existing files</li>
|
||||
* <li>modify/replace files</li>
|
||||
* <li>move files</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return instance of {@link ModifyCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public ModifyCommandBuilder getModifyCommand() {
|
||||
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
|
||||
LOG.debug("create modify command for repository {}", repository);
|
||||
|
||||
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, repository.getId(), eMail);
|
||||
}
|
||||
|
||||
/**
|
||||
* The lookup command executes a lookup which returns additional information for the repository.
|
||||
*
|
||||
* @return instance of {@link LookupCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public LookupCommandBuilder getLookupCommand() {
|
||||
LOG.debug("create lookup command for repository {}", repository);
|
||||
return new LookupCommandBuilder(provider.getLookupCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* The full health check command inspects a repository in a way, that might take a while in contrast to the
|
||||
* light checks executed at startup.
|
||||
*
|
||||
* @return instance of {@link FullHealthCheckCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public FullHealthCheckCommandBuilder getFullCheckCommand() {
|
||||
LOG.debug("create full check command for repository {}", repository);
|
||||
return new FullHealthCheckCommandBuilder(provider.getFullHealthCheckCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* The mirror command creates a 'mirror' of an existing repository (specified by a URL) by copying all data
|
||||
* to the repository of this service. Therefore this repository has to be empty (otherwise the behaviour is
|
||||
* not specified).
|
||||
*
|
||||
* @return instance of {@link MirrorCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.19.0
|
||||
*/
|
||||
public MirrorCommandBuilder getMirrorCommand() {
|
||||
LOG.debug("create mirror command for repository {}", repository);
|
||||
return new MirrorCommandBuilder(provider.getMirrorCommand(), repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock and unlock files.
|
||||
*
|
||||
* @return instance of {@link FileLockCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.26.0
|
||||
*/
|
||||
public FileLockCommandBuilder getLockCommand() {
|
||||
LOG.debug("create lock command for repository {}", repository);
|
||||
return new FileLockCommandBuilder(provider.getFileLockCommand(), repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details for a branch.
|
||||
*
|
||||
* @return instance of {@link BranchDetailsCommand}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.28.0
|
||||
*/
|
||||
public BranchDetailsCommandBuilder getBranchDetailsCommand() {
|
||||
LOG.debug("create branch details command for repository {}", repository);
|
||||
return new BranchDetailsCommandBuilder(repository, provider.getBranchDetailsCommand(), cacheManager);
|
||||
}
|
||||
|
||||
public ChangesetsCommandBuilder getChangesetsCommand() {
|
||||
LOG.debug("create changesets command for repository {}", repository);
|
||||
return new ChangesetsCommandBuilder(repository, provider.getChangesetsCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the command is supported by the repository service.
|
||||
*
|
||||
* @param command command
|
||||
* @return true if the command is supported
|
||||
*/
|
||||
public boolean isSupported(Command command) {
|
||||
return provider.getSupportedCommands().contains(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the feature is supported by the repository service.
|
||||
*
|
||||
* @param feature feature
|
||||
* @return true if the feature is supported
|
||||
* @since 1.25
|
||||
*/
|
||||
public boolean isSupported(Feature feature) {
|
||||
return provider.getSupportedFeatures().contains(feature);
|
||||
}
|
||||
|
||||
public Stream<ScmProtocol> getSupportedProtocols() {
|
||||
return protocolProviders.stream()
|
||||
.filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType()))
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import jakarta.annotation.Nullable;
|
||||
import sonia.scm.repository.spi.RevertCommand;
|
||||
import sonia.scm.repository.spi.RevertCommandRequest;
|
||||
import sonia.scm.repository.util.AuthorUtil;
|
||||
import sonia.scm.user.DisplayUser;
|
||||
import sonia.scm.user.EMail;
|
||||
|
||||
/**
|
||||
* Applies a revert of a chosen changeset onto the given repository/branch combination.
|
||||
*
|
||||
* @since 3.8
|
||||
*/
|
||||
public final class RevertCommandBuilder {
|
||||
|
||||
private final RevertCommand command;
|
||||
private final RevertCommandRequest request;
|
||||
|
||||
@Nullable
|
||||
private final EMail email;
|
||||
|
||||
/**
|
||||
* @param command A {@link RevertCommand} implementation provided by some source.
|
||||
*/
|
||||
public RevertCommandBuilder(RevertCommand command, @Nullable EMail email) {
|
||||
this.command = command;
|
||||
this.email = email;
|
||||
this.request = new RevertCommandRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to set the author of the revert commit manually. If this is omitted, the currently logged-in user will be
|
||||
* used instead. If the given user object does not have an email address, we will use {@link EMail} to compute a
|
||||
* fallback address.
|
||||
*
|
||||
* @param author Author entity.
|
||||
* @return This instance.
|
||||
*/
|
||||
public RevertCommandBuilder setAuthor(DisplayUser author) {
|
||||
request.setAuthor(AuthorUtil.createAuthorWithMailFallback(author, email));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligatory value.
|
||||
*
|
||||
* @param revision Identifier of the revision.
|
||||
* @return This instance.
|
||||
*/
|
||||
public RevertCommandBuilder setRevision(String revision) {
|
||||
request.setRevision(revision);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an optional parameter. Not every SCM system supports branches.
|
||||
* If null or empty and supported by the SCM, the default branch of the repository shall be used.
|
||||
*
|
||||
* @param branch Name of the branch.
|
||||
* @return This instance.
|
||||
*/
|
||||
public RevertCommandBuilder setBranch(String branch) {
|
||||
request.setBranch(branch);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an optional parameter. If null or empty, a default message will be set.
|
||||
*
|
||||
* @param message Particular message.
|
||||
* @return This instance.
|
||||
*/
|
||||
public RevertCommandBuilder setMessage(String message) {
|
||||
request.setMessage(message);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the revert with the given builder parameters.
|
||||
*
|
||||
* @return {@link RevertCommandResult} with information about the executed revert.
|
||||
*/
|
||||
public RevertCommandResult execute() {
|
||||
AuthorUtil.setAuthorIfNotAvailable(request, email);
|
||||
Preconditions.checkArgument(request.isValid(), "Revert request is invalid, request was: %s", request);
|
||||
return command.revert(request);
|
||||
}
|
||||
|
||||
protected RevertCommandRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
/**
|
||||
* Contains the result of an executed revert command.
|
||||
*
|
||||
* @since 3.8
|
||||
*/
|
||||
@Getter
|
||||
public class RevertCommandResult {
|
||||
|
||||
/**
|
||||
* The identifier of the revision after the applied revert.
|
||||
*/
|
||||
private final String revision;
|
||||
/**
|
||||
* A collection of files where conflicts occur.
|
||||
*/
|
||||
private final Collection<String> filesWithConflict;
|
||||
|
||||
/**
|
||||
* Creates a {@link RevertCommandResult}.
|
||||
*
|
||||
* @param revision revision identifier
|
||||
* @param filesWithConflict a collection of files where conflicts occur
|
||||
*/
|
||||
public RevertCommandResult(String revision, Collection<String> filesWithConflict) {
|
||||
this.revision = revision;
|
||||
this.filesWithConflict = filesWithConflict;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to indicate a successful revert.
|
||||
*
|
||||
* @param newHeadRevision id of the newly created revert
|
||||
* @return {@link RevertCommandResult}
|
||||
*/
|
||||
public static RevertCommandResult success(String newHeadRevision) {
|
||||
return new RevertCommandResult(newHeadRevision, emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to indicate a failed revert.
|
||||
*
|
||||
* @param filesWithConflict collection of conflicting files
|
||||
* @return {@link RevertCommandResult}
|
||||
*/
|
||||
public static RevertCommandResult failure(Collection<String> filesWithConflict) {
|
||||
return new RevertCommandResult(null, new HashSet<>(filesWithConflict));
|
||||
}
|
||||
|
||||
/**
|
||||
* If this returns <code>true</code>, the revert was successful. If this returns <code>false</code>, there may have
|
||||
* been problems like a merge conflict after the revert.
|
||||
*/
|
||||
public boolean isSuccessful() {
|
||||
return filesWithConflict.isEmpty() && revision != null;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.CommandNotSupportedException;
|
||||
@@ -26,13 +27,17 @@ import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class is an extension base for SCM system providers to implement command functionalitites.
|
||||
* If unimplemented, the methods within this class throw {@link CommandNotSupportedException}. These are not supposed
|
||||
* to be called if unimplemented for an SCM system.
|
||||
*
|
||||
* @see sonia.scm.repository.api.RepositoryService
|
||||
* @since 1.17
|
||||
*/
|
||||
public abstract class RepositoryServiceProvider implements Closeable
|
||||
{
|
||||
@Slf4j
|
||||
public abstract class RepositoryServiceProvider implements Closeable {
|
||||
|
||||
|
||||
|
||||
public abstract Set<Command> getSupportedCommands();
|
||||
|
||||
|
||||
@@ -41,195 +46,118 @@ public abstract class RepositoryServiceProvider implements Closeable
|
||||
* free resources, close connections or release locks than you have to
|
||||
* override this method.
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
|
||||
// should be implmentented from a service provider
|
||||
public void close() throws IOException {
|
||||
log.warn("warning: close() has been called without implementation from a service provider.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
public BlameCommand getBlameCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.BLAME);
|
||||
}
|
||||
|
||||
|
||||
public BranchesCommand getBranchesCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.BRANCHES);
|
||||
}
|
||||
|
||||
|
||||
public BranchCommand getBranchCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.BRANCH);
|
||||
}
|
||||
|
||||
|
||||
public BrowseCommand getBrowseCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.BROWSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.43
|
||||
*/
|
||||
public BundleCommand getBundleCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.BUNDLE);
|
||||
}
|
||||
|
||||
public CatCommand getCatCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.CAT);
|
||||
}
|
||||
|
||||
|
||||
public DiffCommand getDiffCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.DIFF);
|
||||
}
|
||||
|
||||
public DiffResultCommand getDiffResultCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.DIFF_RESULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.31
|
||||
*/
|
||||
public IncomingCommand getIncomingCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.INCOMING);
|
||||
}
|
||||
|
||||
|
||||
public LogCommand getLogCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.LOG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the corresponding {@link ModificationsCommand} implemented from the Plugins
|
||||
*
|
||||
* @return the corresponding {@link ModificationsCommand} implemented from the Plugins
|
||||
* @throws CommandNotSupportedException if there is no Implementation
|
||||
*/
|
||||
public ModificationsCommand getModificationsCommand() {
|
||||
throw new CommandNotSupportedException(Command.MODIFICATIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.31
|
||||
*/
|
||||
public OutgoingCommand getOutgoingCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.OUTGOING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.31
|
||||
*/
|
||||
public PullCommand getPullCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.PULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.31
|
||||
*/
|
||||
public PushCommand getPushCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.PUSH);
|
||||
}
|
||||
|
||||
|
||||
public Set<Feature> getSupportedFeatures()
|
||||
{
|
||||
public Set<Feature> getSupportedFeatures() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
|
||||
public TagsCommand getTagsCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.TAGS);
|
||||
public BlameCommand getBlameCommand() {
|
||||
throw new CommandNotSupportedException(Command.BLAME);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @since 2.11.0
|
||||
*/
|
||||
public TagCommand getTagCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.TAG);
|
||||
public BranchesCommand getBranchesCommand() {
|
||||
throw new CommandNotSupportedException(Command.BRANCHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.43
|
||||
*/
|
||||
public UnbundleCommand getUnbundleCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.UNBUNDLE);
|
||||
public BranchCommand getBranchCommand() {
|
||||
throw new CommandNotSupportedException(Command.BRANCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
public MergeCommand getMergeCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.MERGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
public ModifyCommand getModifyCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.MODIFY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.10.0
|
||||
*/
|
||||
public LookupCommand getLookupCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.LOOKUP);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.17.0
|
||||
*/
|
||||
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
||||
throw new CommandNotSupportedException(Command.FULL_HEALTH_CHECK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.19.0
|
||||
*/
|
||||
public MirrorCommand getMirrorCommand() {
|
||||
throw new CommandNotSupportedException(Command.MIRROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.26.0
|
||||
*/
|
||||
public FileLockCommand getFileLockCommand() {
|
||||
throw new CommandNotSupportedException(Command.FILE_LOCK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.28.0
|
||||
*/
|
||||
public BranchDetailsCommand getBranchDetailsCommand() {
|
||||
throw new CommandNotSupportedException(Command.BRANCH_DETAILS);
|
||||
}
|
||||
|
||||
public BrowseCommand getBrowseCommand() {
|
||||
throw new CommandNotSupportedException(Command.BROWSE);
|
||||
}
|
||||
|
||||
public BundleCommand getBundleCommand() {
|
||||
throw new CommandNotSupportedException(Command.BUNDLE);
|
||||
}
|
||||
|
||||
public CatCommand getCatCommand() {
|
||||
throw new CommandNotSupportedException(Command.CAT);
|
||||
}
|
||||
|
||||
public ChangesetsCommand getChangesetsCommand() {
|
||||
throw new CommandNotSupportedException(Command.CHANGESETS);
|
||||
}
|
||||
|
||||
public DiffCommand getDiffCommand() {
|
||||
throw new CommandNotSupportedException(Command.DIFF);
|
||||
}
|
||||
|
||||
public DiffResultCommand getDiffResultCommand() {
|
||||
throw new CommandNotSupportedException(Command.DIFF_RESULT);
|
||||
}
|
||||
|
||||
public FileLockCommand getFileLockCommand() {
|
||||
throw new CommandNotSupportedException(Command.FILE_LOCK);
|
||||
}
|
||||
|
||||
public FullHealthCheckCommand getFullHealthCheckCommand() {
|
||||
throw new CommandNotSupportedException(Command.FULL_HEALTH_CHECK);
|
||||
}
|
||||
|
||||
public IncomingCommand getIncomingCommand() {
|
||||
throw new CommandNotSupportedException(Command.INCOMING);
|
||||
}
|
||||
|
||||
public LogCommand getLogCommand() {
|
||||
throw new CommandNotSupportedException(Command.LOG);
|
||||
}
|
||||
|
||||
public LookupCommand getLookupCommand() {
|
||||
throw new CommandNotSupportedException(Command.LOOKUP);
|
||||
}
|
||||
|
||||
public MergeCommand getMergeCommand() {
|
||||
throw new CommandNotSupportedException(Command.MERGE);
|
||||
}
|
||||
|
||||
public MirrorCommand getMirrorCommand() {
|
||||
throw new CommandNotSupportedException(Command.MIRROR);
|
||||
}
|
||||
|
||||
public ModificationsCommand getModificationsCommand() {
|
||||
throw new CommandNotSupportedException(Command.MODIFICATIONS);
|
||||
}
|
||||
|
||||
public ModifyCommand getModifyCommand() {
|
||||
throw new CommandNotSupportedException(Command.MODIFY);
|
||||
}
|
||||
|
||||
public OutgoingCommand getOutgoingCommand() {
|
||||
throw new CommandNotSupportedException(Command.OUTGOING);
|
||||
}
|
||||
|
||||
public PullCommand getPullCommand() {
|
||||
throw new CommandNotSupportedException(Command.PULL);
|
||||
}
|
||||
|
||||
public PushCommand getPushCommand() {
|
||||
throw new CommandNotSupportedException(Command.PUSH);
|
||||
}
|
||||
|
||||
public RevertCommand getRevertCommand() {
|
||||
throw new CommandNotSupportedException(Command.REVERT);
|
||||
}
|
||||
|
||||
public TagsCommand getTagsCommand() {
|
||||
throw new CommandNotSupportedException(Command.TAGS);
|
||||
}
|
||||
|
||||
public TagCommand getTagCommand() {
|
||||
throw new CommandNotSupportedException(Command.TAG);
|
||||
}
|
||||
|
||||
public UnbundleCommand getUnbundleCommand() {
|
||||
throw new CommandNotSupportedException(Command.UNBUNDLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
/**
|
||||
* @deprecated This interface may get removed at some point in the future.
|
||||
* @since 1.17
|
||||
*/
|
||||
@Deprecated(since = "3.8")
|
||||
public interface Resetable
|
||||
{
|
||||
|
||||
public void reset();
|
||||
void reset();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.api.RevertCommandResult;
|
||||
|
||||
/**
|
||||
* Removes the changes from a particular changeset as a revert. This, in turn, will result a new changeset.
|
||||
*
|
||||
* @since 3.8
|
||||
*/
|
||||
public interface RevertCommand {
|
||||
|
||||
/**
|
||||
* Executes a revert.
|
||||
* @param request parameter set for this command.
|
||||
* @see RevertCommand
|
||||
* @return result set of the executed command (see {@link RevertCommandResult}).
|
||||
*/
|
||||
RevertCommandResult revert(RevertCommandRequest request);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import sonia.scm.Validateable;
|
||||
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This class contains the information to run {@link RevertCommand#revert(RevertCommandRequest)}.
|
||||
|
||||
* @since 3.8
|
||||
*/
|
||||
@Setter
|
||||
@ToString
|
||||
public class RevertCommandRequest implements Validateable, CommandWithAuthor {
|
||||
|
||||
@Getter
|
||||
private Person author;
|
||||
|
||||
@Getter
|
||||
private String revision;
|
||||
|
||||
/**
|
||||
* Reverts can be signed with a GPG key. This is set as <tt>true</tt> by default.
|
||||
*/
|
||||
@Getter
|
||||
private boolean sign = true;
|
||||
|
||||
private String branch;
|
||||
|
||||
private String message;
|
||||
|
||||
public Optional<String> getBranch() {
|
||||
return Optional.ofNullable(branch);
|
||||
}
|
||||
|
||||
public Optional<String> getMessage() {
|
||||
return Optional.ofNullable(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
boolean validBranch = branch == null || !branch.isEmpty();
|
||||
boolean validMessage = message == null || !message.isEmpty();
|
||||
return revision != null && author != null && validBranch && validMessage;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,29 +20,61 @@ import jakarta.annotation.Nullable;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.user.DisplayUser;
|
||||
import sonia.scm.user.EMail;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
/**
|
||||
* Contains convenience methods to manage {@link CommandWithAuthor} classes.
|
||||
*/
|
||||
public class AuthorUtil {
|
||||
|
||||
/**
|
||||
* @see AuthorUtil#createAuthorFromSubject(EMail)
|
||||
* @param request {@link CommandWithAuthor}
|
||||
*/
|
||||
public static void setAuthorIfNotAvailable(CommandWithAuthor request) {
|
||||
setAuthorIfNotAvailable(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AuthorUtil#createAuthorFromSubject(EMail)
|
||||
* @param request {@link CommandWithAuthor}
|
||||
* @param eMail {@link EMail}
|
||||
*/
|
||||
public static void setAuthorIfNotAvailable(CommandWithAuthor request, @Nullable EMail eMail) {
|
||||
if (request.getAuthor() == null) {
|
||||
request.setAuthor(createAuthorFromSubject(eMail));
|
||||
}
|
||||
}
|
||||
|
||||
private static Person createAuthorFromSubject(@Nullable EMail eMail) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
User user = subject.getPrincipals().oneByType(User.class);
|
||||
/**
|
||||
* Depending on the mail input, the {@link Person} is either created by the given nullable {@link EMail}
|
||||
* or the information from {@link DisplayUser} if the mail remains null.
|
||||
* @param user {@link DisplayUser}
|
||||
* @param eMail (nullable) {@link EMail}
|
||||
* @return {@link Person}
|
||||
*/
|
||||
public static Person createAuthorWithMailFallback(DisplayUser user, @Nullable EMail eMail) {
|
||||
String name = user.getDisplayName();
|
||||
String mailAddress = eMail != null ? eMail.getMailOrFallback(user) : user.getMail();
|
||||
return new Person(name, mailAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an author from the Apache Shiro {@link Subject} given by the {@link SecurityUtils}.
|
||||
* @param eMail {@link EMail}
|
||||
* @return {@link Person}
|
||||
*/
|
||||
private static Person createAuthorFromSubject(@Nullable EMail eMail) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
User user = subject.getPrincipals().oneByType(User.class);
|
||||
return createAuthorWithMailFallback(DisplayUser.from(user), eMail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Command whose execution includes an author as a {@link Person}.
|
||||
*/
|
||||
public interface CommandWithAuthor {
|
||||
Person getAuthor();
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
package sonia.scm.update;
|
||||
|
||||
export const SecondaryNavigationContext = React.createContext(false);
|
||||
import sonia.scm.repository.RepositoryPermissionHolder;
|
||||
|
||||
public interface RepositoryPermissionUpdater {
|
||||
void removePermission(RepositoryPermissionHolder permissionHolder, String permissionName);
|
||||
}
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
package sonia.scm.user;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import sonia.scm.ReducedModelObject;
|
||||
|
||||
@EqualsAndHashCode
|
||||
public class DisplayUser implements ReducedModelObject {
|
||||
|
||||
private final String id;
|
||||
|
||||
@@ -40,6 +40,7 @@ public class VndMediaType {
|
||||
public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX;
|
||||
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
|
||||
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
|
||||
public static final String REVERT = PREFIX + "revert" + SUFFIX;
|
||||
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
|
||||
public static final String TAG = PREFIX + "tag" + SUFFIX;
|
||||
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.spi.RevertCommand;
|
||||
import sonia.scm.repository.spi.RevertCommandRequest;
|
||||
import sonia.scm.user.DisplayUser;
|
||||
import sonia.scm.user.EMail;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RevertCommandBuilderTest {
|
||||
|
||||
@Mock
|
||||
private RevertCommand revertCommand;
|
||||
@Mock
|
||||
private EMail eMail;
|
||||
|
||||
@InjectMocks
|
||||
private RevertCommandBuilder revertCommandBuilder;
|
||||
|
||||
@BeforeEach
|
||||
void prepareCommandBuilder() {
|
||||
revertCommandBuilder.setRevision("irrelevant");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseMailAddressFromEMailFallback() {
|
||||
User user = new User("dent", "Arthur Dent", null);
|
||||
DisplayUser author = DisplayUser.from(user);
|
||||
when(eMail.getMailOrFallback(author)).thenReturn("dent@hitchhiker.com");
|
||||
revertCommandBuilder.setAuthor(author);
|
||||
revertCommandBuilder.execute();
|
||||
|
||||
verify(revertCommand).revert(argThat(revertCommandRequest -> {
|
||||
assertThat(revertCommandRequest.getAuthor().getMail()).isEqualTo("dent@hitchhiker.com");
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetAuthorFromShiroSubjectIfNotSet() {
|
||||
User user = new User("dent", "Arthur Dent", null);DisplayUser author = DisplayUser.from(user);
|
||||
when(eMail.getMailOrFallback(author)).thenReturn("dent@hitchhiker.com");
|
||||
mockLoggedInUser(user);
|
||||
revertCommandBuilder.execute();
|
||||
RevertCommandRequest request = revertCommandBuilder.getRequest();
|
||||
|
||||
assertThat(request.getAuthor().getName()).isEqualTo("Arthur Dent");
|
||||
assertThat(request.getAuthor().getMail()).isEqualTo("dent@hitchhiker.com");
|
||||
|
||||
mockLogout();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetAllFieldsInRequest() {
|
||||
User user = new User("dent", "Arthur Dent", null);
|
||||
DisplayUser author = DisplayUser.from(user);
|
||||
when(eMail.getMailOrFallback(author)).thenReturn("dent@hitchhiker.com");
|
||||
revertCommandBuilder.setAuthor(author);
|
||||
revertCommandBuilder.setBranch("someBranch");
|
||||
revertCommandBuilder.setMessage("someMessage");
|
||||
|
||||
RevertCommandRequest request = revertCommandBuilder.getRequest();
|
||||
|
||||
assertThat(request.getAuthor().getName()).isEqualTo(author.getDisplayName());
|
||||
assertThat(request.getBranch()).contains("someBranch");
|
||||
assertThat(request.getMessage()).contains("someMessage");
|
||||
assertThat(request.getRevision()).isEqualTo("irrelevant");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExecuteInvalidRequestDueToEmptyBranch() {
|
||||
User user = new User("dent", "Arthur Dent", "dent@hitchhiker.com");
|
||||
revertCommandBuilder.setAuthor(DisplayUser.from(user));
|
||||
revertCommandBuilder.setBranch("");
|
||||
assertThatThrownBy(() -> revertCommandBuilder.execute())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Revert request is invalid, request was: RevertCommandRequest(author=Arthur Dent, revision=irrelevant, sign=true, branch=Optional[], message=Optional.empty)");
|
||||
}
|
||||
|
||||
private void mockLoggedInUser(User loggedInUser) {
|
||||
Subject subject = mock(Subject.class);
|
||||
ThreadContext.bind(subject);
|
||||
PrincipalCollection principals = mock(PrincipalCollection.class);
|
||||
when(subject.getPrincipals()).thenReturn(principals);
|
||||
when(principals.oneByType(User.class)).thenReturn(loggedInUser);
|
||||
}
|
||||
|
||||
private void mockLogout() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.user.DisplayUser;
|
||||
import sonia.scm.user.EMail;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
@@ -54,9 +55,9 @@ class AuthorUtilTest {
|
||||
@Test
|
||||
void shouldCreateMailAddressFromEmail() {
|
||||
User trillian = new User("trillian");
|
||||
when(subject.getPrincipals().oneByType(User.class)).thenReturn(trillian);
|
||||
when(eMail.getMailOrFallback(trillian)).thenReturn("tricia@hitchhicker.com");
|
||||
|
||||
when(subject.getPrincipals().oneByType(User.class)).thenReturn(trillian);
|
||||
when(eMail.getMailOrFallback(DisplayUser.from(trillian))).thenReturn("tricia@hitchhicker.com");
|
||||
Command command = new Command(null);
|
||||
AuthorUtil.setAuthorIfNotAvailable(command, eMail);
|
||||
|
||||
|
||||
@@ -130,6 +130,9 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
}
|
||||
|
||||
private void modifyLocation(String repositoryId, Path newPath, Modifier modifier) throws RepositoryStorageException {
|
||||
if (newPath.toFile().exists() && !newPath.toFile().canWrite()) {
|
||||
throw new RepositoryStorageException("cannot create repository at new path " + newPath + "; path is not writable");
|
||||
}
|
||||
maintenanceCallbacks.fire().downForMaintenance(new DownForMaintenanceContext(repositoryId));
|
||||
Path oldPath = pathById.get(repositoryId);
|
||||
pathById.remove(repositoryId);
|
||||
|
||||
@@ -16,13 +16,10 @@
|
||||
|
||||
package sonia.scm.repository.xml;
|
||||
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.inject.Singleton;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
@@ -37,7 +34,6 @@ import sonia.scm.store.StoreReadOnlyException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
@@ -45,8 +41,6 @@ import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
@@ -62,9 +56,6 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
private final Map<NamespaceAndName, Repository> byNamespaceAndName;
|
||||
private final ReadWriteLock byNamespaceLock = new ReentrantReadWriteLock();
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XmlRepositoryDAO.class);
|
||||
|
||||
|
||||
@Inject
|
||||
public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem, RepositoryExportingCheck repositoryExportingCheck) {
|
||||
this.repositoryLocationResolver = repositoryLocationResolver;
|
||||
@@ -99,10 +90,10 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
pathRepositoryLocationResolverInstance.forAllLocations((repositoryId, repositoryPath) -> {
|
||||
try {
|
||||
Repository repository = metadataStore.read(repositoryPath);
|
||||
if (byNamespaceAndName.containsKey(repository.getNamespaceAndName())) {
|
||||
LOG.warn("Duplicate repository found. Adding suffix DUPLICATE to repository {}", repository);
|
||||
repository.setName(repository.getName() + "-" + repositoryId + "-DUPLICATE");
|
||||
}
|
||||
if (byNamespaceAndName.containsKey(repository.getNamespaceAndName())) {
|
||||
log.warn("Duplicate repository found. Adding suffix DUPLICATE to repository {}", repository);
|
||||
repository.setName(repository.getName() + "-" + repositoryId + "-DUPLICATE");
|
||||
}
|
||||
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
|
||||
byId.put(repositoryId, repository);
|
||||
} catch (InternalRepositoryException e) {
|
||||
|
||||
@@ -233,8 +233,6 @@ class PathBasedRepositoryLocationResolverTest {
|
||||
assertThat(newPath).exists();
|
||||
assertThat(oldPath).exists();
|
||||
assertThat(resolverWithExistingData.create(Path.class).getLocation("existingId_1")).isEqualTo(oldPath);
|
||||
verify(maintenanceCallback).downForMaintenance(new DownForMaintenanceContext("existingId_1"));
|
||||
verify(maintenanceCallback).upAfterMaintenance(new UpAfterMaintenanceContext("existingId_1", oldPath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,8 @@ class AnonymousAccessITCase {
|
||||
.addNull("proxyUser")
|
||||
.add("realmDescription", "SONIA :: SCM Manager")
|
||||
.add("skipFailedAuthenticators", false)
|
||||
.add("jwtExpirationInH", 1)
|
||||
.add("enabledJwtEndless", false)
|
||||
.build().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ class DiffITCase {
|
||||
assertDiffsAreEqual(svnDiff, expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@RetryingTest(3)
|
||||
void svnUpdateFileDiffShouldBeConvertedToGitDiff() throws IOException {
|
||||
RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
|
||||
RepositoryUtil.createAndCommitFile(gitRepositoryClient, ADMIN_USERNAME, "a.txt", "content of a");
|
||||
@@ -206,7 +206,7 @@ class DiffITCase {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@RetryingTest(3)
|
||||
void svnRenameChangesDiffShouldBeConvertedToGitDiff() throws IOException, URISyntaxException {
|
||||
String fileName = "a.txt";
|
||||
RepositoryUtil.createAndCommitFile(svnRepositoryClient, ADMIN_USERNAME, fileName, "content of a");
|
||||
|
||||
@@ -281,7 +281,9 @@ public class TestData {
|
||||
" \"loginInfoUrl\": \"https://login-info.scm-manager.org/api/v1/login-info\",\n" +
|
||||
" \"releaseFeedUrl\": \"https://scm-manager.org/download/rss.xml\",\n" +
|
||||
" \"mailDomainName\": \"scm-manager.local\", \n" +
|
||||
" \"enabledApiKeys\": \"true\"\n" +
|
||||
" \"enabledApiKeys\": \"true\",\n" +
|
||||
" \"jwtExpirationInH\": 1,\n" +
|
||||
" \"enabledJwtEndless\": false\n" +
|
||||
"}")
|
||||
.put(createResourceUrl("config"))
|
||||
.then()
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#
|
||||
|
||||
# Create minimal java version
|
||||
FROM alpine:3.19.3 as jre-build
|
||||
FROM alpine:3.21.3 as jre-build
|
||||
|
||||
RUN set -x \
|
||||
&& apk add --no-cache openjdk17-jdk openjdk17-jmods binutils \
|
||||
@@ -31,7 +31,7 @@ RUN set -x \
|
||||
# ---
|
||||
|
||||
# SCM-Manager runtime
|
||||
FROM alpine:3.19.3 as runtime
|
||||
FROM alpine:3.21.3 as runtime
|
||||
|
||||
ENV SCM_HOME /var/lib/scm
|
||||
ENV CACHE_DIR /var/cache/scm/work
|
||||
|
||||
@@ -28,7 +28,7 @@ RUN jlink \
|
||||
# ---
|
||||
|
||||
# SCM-Manager runtime
|
||||
FROM debian:11.7-slim as runtime
|
||||
FROM --platform=linux/amd64 debian:11.11-slim as runtime
|
||||
|
||||
ENV SCM_HOME /var/lib/scm
|
||||
ENV CACHE_DIR /var/cache/scm/work
|
||||
|
||||
@@ -18,7 +18,7 @@ plugins {
|
||||
id 'org.scm-manager.smp' version '0.17.0'
|
||||
}
|
||||
|
||||
def jgitVersion = '6.7.0.202309050840-r-scm1-jakarta'
|
||||
def jgitVersion = '7.1.0.202411261347-r-scm1'
|
||||
|
||||
dependencies {
|
||||
// required by scm-it
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@scm-manager/scm-git-plugin",
|
||||
"private": true,
|
||||
"version": "3.6.1-SNAPSHOT",
|
||||
"version": "3.7.5-SNAPSHOT",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "./src/main/js/index.ts",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scm-manager/ui-plugins": "3.6.1-SNAPSHOT"
|
||||
"@scm-manager/ui-plugins": "3.7.5-SNAPSHOT"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/babel-preset": "^2.13.1",
|
||||
|
||||
@@ -25,10 +25,11 @@ public class GitConfigHelper {
|
||||
private static final String CONFIG_SECTION_SCMM = "scmm";
|
||||
private static final String CONFIG_KEY_REPOSITORY_ID = "repositoryid";
|
||||
|
||||
public void createScmmConfig(Repository repository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
|
||||
StoredConfig config = gitRepository.getConfig();
|
||||
config.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, repository.getId());
|
||||
config.save();
|
||||
public void createScmmConfig(Repository scmmRepository, org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
|
||||
StoredConfig gitConfig = gitRepository.getConfig();
|
||||
gitConfig.setString(CONFIG_SECTION_SCMM, null, CONFIG_KEY_REPOSITORY_ID, scmmRepository.getId());
|
||||
gitConfig.setBoolean("uploadpack", null, "allowFilter", true);
|
||||
gitConfig.save();
|
||||
}
|
||||
|
||||
public String getRepositoryId(StoredConfig gitConfig) {
|
||||
|
||||
@@ -48,11 +48,10 @@ public class GitHeadModifier {
|
||||
* repositories head points already to the given branch.
|
||||
*
|
||||
* @param repository repository to modify
|
||||
* @param newHead branch which should be the new head of the repository
|
||||
*
|
||||
* @param newHead branch which should be the new head of the repository
|
||||
* @return {@code true} if the head has changed
|
||||
*/
|
||||
public boolean ensure(Repository repository, String newHead) {
|
||||
public boolean ensure(Repository repository, String newHead) {
|
||||
try (org.eclipse.jgit.lib.Repository gitRepository = open(repository)) {
|
||||
String currentHead = resolve(gitRepository);
|
||||
if (!Objects.equals(currentHead, newHead)) {
|
||||
@@ -65,8 +64,8 @@ public class GitHeadModifier {
|
||||
}
|
||||
|
||||
private String resolve(org.eclipse.jgit.lib.Repository gitRepository) throws IOException {
|
||||
Ref ref = gitRepository.getRefDatabase().getRef(Constants.HEAD);
|
||||
if ( ref.isSymbolic() ) {
|
||||
Ref ref = gitRepository.getRefDatabase().findRef(Constants.HEAD);
|
||||
if (ref.isSymbolic()) {
|
||||
ref = ref.getTarget();
|
||||
}
|
||||
return GitUtil.getBranch(ref);
|
||||
|
||||
@@ -30,13 +30,13 @@ import sonia.scm.plugin.Extension;
|
||||
*/
|
||||
@Extension
|
||||
@EagerSingleton
|
||||
public class GitRepositoryModifyListener {
|
||||
public class GitRepositoryConfigChangeListener {
|
||||
|
||||
private final GitHeadModifier headModifier;
|
||||
private final GitRepositoryConfigStoreProvider storeProvider;
|
||||
|
||||
@Inject
|
||||
public GitRepositoryModifyListener(GitHeadModifier headModifier, GitRepositoryConfigStoreProvider storeProvider) {
|
||||
public GitRepositoryConfigChangeListener(GitHeadModifier headModifier, GitRepositoryConfigStoreProvider storeProvider) {
|
||||
this.headModifier = headModifier;
|
||||
this.storeProvider = storeProvider;
|
||||
}
|
||||
@@ -71,10 +71,10 @@ import static java.util.Optional.ofNullable;
|
||||
|
||||
public final class GitUtil {
|
||||
|
||||
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
|
||||
public static final String REF_HEAD = "HEAD";
|
||||
public static final String REF_HEAD_PREFIX = "refs/heads/";
|
||||
public static final String REF_MAIN = "main";
|
||||
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
|
||||
private static final String DIRECTORY_DOTGIT = ".git";
|
||||
private static final String DIRECTORY_OBJETCS = "objects";
|
||||
private static final String DIRECTORY_REFS = "refs";
|
||||
@@ -84,15 +84,13 @@ public final class GitUtil {
|
||||
private static final String REMOTE_REF = "refs/remote/scm/%s/%s";
|
||||
private static final int TIMEOUT = 5;
|
||||
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
|
||||
private static final String REF_SPEC = "refs/heads/*:refs/heads/*";
|
||||
|
||||
private static final String GPG_HEADER = "-----BEGIN PGP SIGNATURE-----";
|
||||
|
||||
private GitUtil() {
|
||||
}
|
||||
|
||||
|
||||
public static void close(org.eclipse.jgit.lib.Repository repo) {
|
||||
if (repo != null) {
|
||||
repo.close();
|
||||
@@ -181,7 +179,6 @@ public final class GitUtil {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static String getBranch(Ref ref) {
|
||||
String branch = null;
|
||||
|
||||
@@ -234,14 +231,11 @@ public final class GitUtil {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
|
||||
String branchName)
|
||||
throws IOException {
|
||||
Ref ref = null;
|
||||
if (!branchName.startsWith(REF_HEAD)) {
|
||||
branchName = PREFIX_HEADS.concat(branchName);
|
||||
}
|
||||
branchName = getRevString(branchName);
|
||||
|
||||
checkBranchName(repo, branchName);
|
||||
|
||||
@@ -258,6 +252,13 @@ public final class GitUtil {
|
||||
return ref;
|
||||
}
|
||||
|
||||
public static String getRevString(String branchName) {
|
||||
if (!branchName.startsWith(REF_HEAD)) {
|
||||
return PREFIX_HEADS.concat(branchName);
|
||||
}
|
||||
return branchName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.5.0
|
||||
*/
|
||||
@@ -286,22 +287,43 @@ public final class GitUtil {
|
||||
/**
|
||||
* 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 jgit repository
|
||||
* @param revWalk rev walk
|
||||
* @param ref commit/tag ref
|
||||
* @return {@link RevCommit}
|
||||
* @throws IOException exception
|
||||
*/
|
||||
public static RevCommit getCommit(org.eclipse.jgit.lib.Repository repository,
|
||||
RevWalk revWalk, Ref ref)
|
||||
throws IOException {
|
||||
RevCommit commit = null;
|
||||
ObjectId id = ref.getPeeledObjectId();
|
||||
|
||||
if (id == null) {
|
||||
id = ref.getObjectId();
|
||||
}
|
||||
|
||||
return getCommit(repository, revWalk, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the commit for the given object id. The id is expected to be a commit and not a tag.
|
||||
*
|
||||
* @param repository jgit repository
|
||||
* @param revWalk rev walk
|
||||
* @param id commit id
|
||||
* @return {@link RevCommit}
|
||||
* @throws IOException exception
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public static RevCommit getCommit(org.eclipse.jgit.lib.Repository repository,
|
||||
RevWalk revWalk, ObjectId id) throws IOException {
|
||||
RevCommit commit = null;
|
||||
|
||||
if (id != null) {
|
||||
if (revWalk == null) {
|
||||
revWalk = new RevWalk(repository);
|
||||
}
|
||||
|
||||
commit = revWalk.parseCommit(id);
|
||||
}
|
||||
|
||||
@@ -325,7 +347,6 @@ public final class GitUtil {
|
||||
return tag;
|
||||
}
|
||||
|
||||
|
||||
public static long getCommitTime(RevCommit commit) {
|
||||
long date = commit.getCommitTime();
|
||||
|
||||
@@ -334,7 +355,6 @@ public final class GitUtil {
|
||||
return date;
|
||||
}
|
||||
|
||||
|
||||
public static String getId(AnyObjectId objectId) {
|
||||
String id = Util.EMPTY_STRING;
|
||||
|
||||
@@ -345,7 +365,6 @@ public final class GitUtil {
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
public static Ref getRefForCommit(org.eclipse.jgit.lib.Repository repository,
|
||||
ObjectId id)
|
||||
throws IOException {
|
||||
@@ -410,7 +429,6 @@ public final class GitUtil {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
|
||||
public static ObjectId getRevisionId(org.eclipse.jgit.lib.Repository repo,
|
||||
String revision)
|
||||
throws IOException {
|
||||
@@ -425,7 +443,6 @@ public final class GitUtil {
|
||||
return revId;
|
||||
}
|
||||
|
||||
|
||||
public static String getScmRemoteRefName(Repository repository,
|
||||
Ref localBranch) {
|
||||
return getScmRemoteRefName(repository, localBranch.getName());
|
||||
@@ -458,7 +475,6 @@ public final class GitUtil {
|
||||
return tagName;
|
||||
}
|
||||
|
||||
|
||||
public static String getTagName(Ref ref) {
|
||||
String name = ref.getName();
|
||||
|
||||
@@ -469,8 +485,6 @@ public final class GitUtil {
|
||||
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();
|
||||
|
||||
@@ -18,16 +18,18 @@ package sonia.scm.repository;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import org.eclipse.jgit.api.errors.CanceledException;
|
||||
import org.eclipse.jgit.lib.CommitBuilder;
|
||||
import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
|
||||
import org.eclipse.jgit.lib.GpgConfig;
|
||||
import org.eclipse.jgit.lib.GpgSignature;
|
||||
import org.eclipse.jgit.lib.GpgSigner;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.lib.Signer;
|
||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||
import sonia.scm.security.GPG;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ScmGpgSigner extends GpgSigner {
|
||||
public class ScmGpgSigner implements Signer {
|
||||
|
||||
private final GPG gpg;
|
||||
|
||||
@@ -37,17 +39,13 @@ public class ScmGpgSigner extends GpgSigner {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sign(CommitBuilder commitBuilder, String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException {
|
||||
try {
|
||||
final byte[] signature = this.gpg.getPrivateKey().sign(commitBuilder.build());
|
||||
commitBuilder.setGpgSignature(new GpgSignature(signature));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
public GpgSignature sign(Repository repository, GpgConfig gpgConfig, byte[] bytes, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) throws CanceledException, IOException, UnsupportedSigningFormatException {
|
||||
final byte[] signature = this.gpg.getPrivateKey().sign(bytes);
|
||||
return new GpgSignature(signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canLocateSigningKey(String keyId, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException {
|
||||
public boolean canLocateSigningKey(Repository repository, GpgConfig gpgConfig, PersonIdent personIdent, String s, CredentialsProvider credentialsProvider) throws CanceledException {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ package sonia.scm.repository;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.servlet.ServletContextEvent;
|
||||
import jakarta.servlet.ServletContextListener;
|
||||
import org.eclipse.jgit.lib.GpgSigner;
|
||||
import org.eclipse.jgit.lib.GpgConfig;
|
||||
import org.eclipse.jgit.lib.Signers;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
@Extension
|
||||
@@ -34,7 +35,7 @@ public class ScmGpgSignerInitializer implements ServletContextListener {
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent servletContextEvent) {
|
||||
GpgSigner.setDefault(scmGpgSigner);
|
||||
Signers.set(GpgConfig.GpgFormat.OPENPGP, scmGpgSigner);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -78,7 +78,7 @@ class AbstractGitCommand {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
Repository open() throws IOException {
|
||||
Repository open() {
|
||||
return context.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.eclipse.jgit.api.errors.CanceledException;
|
||||
import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
|
||||
import org.eclipse.jgit.lib.CommitBuilder;
|
||||
import org.eclipse.jgit.lib.GpgConfig;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectInserter;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.RefUpdate;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.lib.Signer;
|
||||
import org.eclipse.jgit.lib.Signers;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||
import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
@Slf4j
|
||||
class CommitHelper {
|
||||
|
||||
private final Repository repository;
|
||||
private final GitContext context;
|
||||
private final RepositoryManager repositoryManager;
|
||||
private final GitRepositoryHookEventFactory eventFactory;
|
||||
|
||||
CommitHelper(GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
|
||||
this.repository = context.open();
|
||||
this.context = context;
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.eventFactory = eventFactory;
|
||||
}
|
||||
|
||||
ObjectId createCommit(ObjectId treeId,
|
||||
Person author,
|
||||
Person committer,
|
||||
String message,
|
||||
boolean sign,
|
||||
ObjectId... parentCommitIds) throws IOException, CanceledException, UnsupportedSigningFormatException {
|
||||
return createCommit(treeId, createPersonIdent(author), committer, message, sign, parentCommitIds);
|
||||
}
|
||||
|
||||
ObjectId createCommit(ObjectId treeId,
|
||||
PersonIdent author,
|
||||
Person committer,
|
||||
String message,
|
||||
boolean sign,
|
||||
ObjectId... parentCommitIds) throws IOException, CanceledException, UnsupportedSigningFormatException {
|
||||
log.trace("create commit for tree {} and parent ids {} in repository {}", treeId, parentCommitIds, context.getRepository());
|
||||
try (ObjectInserter inserter = repository.newObjectInserter()) {
|
||||
CommitBuilder commitBuilder = new CommitBuilder();
|
||||
commitBuilder.setTreeId(treeId);
|
||||
commitBuilder.setParentIds(parentCommitIds);
|
||||
commitBuilder.setAuthor(author);
|
||||
commitBuilder.setCommitter(createPersonIdent(committer));
|
||||
commitBuilder.setMessage(message);
|
||||
if (sign) {
|
||||
sign(commitBuilder, createPersonIdent(committer));
|
||||
}
|
||||
ObjectId commitId = inserter.insert(commitBuilder);
|
||||
inserter.flush();
|
||||
log.trace("created commit with id {}", commitId);
|
||||
return commitId;
|
||||
}
|
||||
}
|
||||
|
||||
private PersonIdent createPersonIdent(Person person) {
|
||||
if (person == null) {
|
||||
User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
|
||||
return new PersonIdent(currentUser.getDisplayName(), currentUser.getMail());
|
||||
}
|
||||
return new PersonIdent(person.getName(), person.getMail());
|
||||
}
|
||||
|
||||
private void sign(CommitBuilder commit, PersonIdent committer)
|
||||
throws CanceledException, IOException, UnsupportedSigningFormatException {
|
||||
log.trace("sign commit");
|
||||
GpgConfig gpgConfig = new GpgConfig(repository.getConfig());
|
||||
Signer signer = Signers.get(gpgConfig.getKeyFormat());
|
||||
signer.signObject(repository, gpgConfig, commit, committer, "SCM-MANAGER-DEFAULT-KEY", CredentialsProvider.getDefault());
|
||||
}
|
||||
|
||||
void updateBranch(String branchName, ObjectId newCommitId, ObjectId expectedOldObjectId) {
|
||||
log.trace("update branch {} with new commit id {} in repository {}", branchName, newCommitId, context.getRepository());
|
||||
try {
|
||||
RevCommit newCommit = findNewCommit(newCommitId);
|
||||
firePreCommitHook(branchName, newCommit);
|
||||
doUpdate(branchName, newCommitId, expectedOldObjectId);
|
||||
firePostCommitHook(branchName, newCommit);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not update branch " + branchName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private RevCommit findNewCommit(ObjectId newCommitId) throws IOException {
|
||||
RevCommit newCommit;
|
||||
try (RevWalk revWalk = new RevWalk(repository)) {
|
||||
newCommit = revWalk.parseCommit(newCommitId);
|
||||
}
|
||||
return newCommit;
|
||||
}
|
||||
|
||||
private void firePreCommitHook(String branchName, RevCommit newCommit) {
|
||||
repositoryManager.fireHookEvent(
|
||||
eventFactory.createPreReceiveEvent(
|
||||
context,
|
||||
List.of(branchName),
|
||||
emptyList(),
|
||||
() -> List.of(newCommit)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void doUpdate(String branchName, ObjectId newCommitId, ObjectId expectedOldObjectId) throws IOException {
|
||||
RefUpdate refUpdate = repository.updateRef(GitUtil.getRevString(branchName));
|
||||
if (newCommitId == null) {
|
||||
refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
|
||||
} else {
|
||||
refUpdate.setExpectedOldObjectId(expectedOldObjectId);
|
||||
}
|
||||
refUpdate.setNewObjectId(newCommitId);
|
||||
refUpdate.setForceUpdate(false);
|
||||
RefUpdate.Result result = refUpdate.update();
|
||||
|
||||
if (isSuccessfulUpdate(expectedOldObjectId, result)) {
|
||||
throw new ConcurrentModificationException(entity("branch", branchName).in(context.getRepository()).build());
|
||||
}
|
||||
}
|
||||
|
||||
private void firePostCommitHook(String branchName, RevCommit newCommit) {
|
||||
repositoryManager.fireHookEvent(
|
||||
eventFactory.createPostReceiveEvent(
|
||||
context,
|
||||
List.of(branchName),
|
||||
emptyList(),
|
||||
() -> List.of(newCommit)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isSuccessfulUpdate(ObjectId expectedOldObjectId, RefUpdate.Result result) {
|
||||
return result != RefUpdate.Result.FAST_FORWARD && !(expectedOldObjectId == null && result == RefUpdate.Result.NEW);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
|
||||
log.debug("got exception for invalid branch name {}", request.getNewBranch(), e);
|
||||
doThrow().violation("Invalid branch name", "name").when(true);
|
||||
return null;
|
||||
} catch (GitAPIException | IOException ex) {
|
||||
} catch (GitAPIException ex) {
|
||||
throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex);
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
|
||||
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
|
||||
} catch (CannotDeleteCurrentBranchException e) {
|
||||
throw new CannotDeleteDefaultBranchException(context.getRepository(), branchName);
|
||||
} catch (GitAPIException | IOException ex) {
|
||||
} catch (GitAPIException ex) {
|
||||
throw new InternalRepositoryException(entity(context.getRepository()), String.format("Could not delete branch: %s", branchName));
|
||||
}
|
||||
}
|
||||
@@ -161,12 +161,7 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
|
||||
|
||||
@Override
|
||||
public HookChangesetProvider getChangesetProvider() {
|
||||
Repository gitRepo;
|
||||
try {
|
||||
gitRepo = context.open();
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "failed to open repository for post receive hook after internal change", e);
|
||||
}
|
||||
Repository gitRepo = context.open();
|
||||
|
||||
Collection<ReceiveCommand> receiveCommands = asList(createReceiveCommand());
|
||||
return x -> {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Branch> getBranches() throws IOException {
|
||||
public List<Branch> getBranches() {
|
||||
Git git = createGit();
|
||||
|
||||
String defaultBranchName = determineDefaultBranchName(git);
|
||||
@@ -72,7 +72,7 @@ public class GitBranchesCommand extends AbstractGitCommand implements BranchesCo
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Git createGit() throws IOException {
|
||||
Git createGit() {
|
||||
return new Git(open());
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||
import sonia.scm.repository.GitConfig;
|
||||
import sonia.scm.repository.GitRepositoryConfig;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryProvider;
|
||||
|
||||
@@ -62,13 +63,17 @@ public class GitContext implements Closeable, RepositoryProvider
|
||||
}
|
||||
|
||||
|
||||
public org.eclipse.jgit.lib.Repository open() throws IOException
|
||||
public org.eclipse.jgit.lib.Repository open()
|
||||
{
|
||||
if (gitRepository == null)
|
||||
{
|
||||
logger.trace("open git repository {}", directory);
|
||||
|
||||
gitRepository = GitUtil.open(directory);
|
||||
try {
|
||||
gitRepository = GitUtil.open(directory);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "could not open git repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
return gitRepository;
|
||||
|
||||
@@ -16,38 +16,41 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.MergeCommand;
|
||||
import org.eclipse.jgit.api.MergeResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
@Slf4j
|
||||
class GitFastForwardIfPossible {
|
||||
|
||||
class GitFastForwardIfPossible extends GitMergeStrategy {
|
||||
private final MergeCommandRequest request;
|
||||
private final MergeHelper mergeHelper;
|
||||
private final GitMergeCommit fallbackMerge;
|
||||
private final CommitHelper commitHelper;
|
||||
private final Repository repository;
|
||||
|
||||
private GitMergeStrategy fallbackMerge;
|
||||
|
||||
GitFastForwardIfPossible(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
|
||||
super(clone, request, context, repository);
|
||||
fallbackMerge = new GitMergeCommit(clone, request, context, repository);
|
||||
GitFastForwardIfPossible(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
|
||||
this.request = request;
|
||||
this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
|
||||
this.fallbackMerge = new GitMergeCommit(request, context, repositoryManager, eventFactory);
|
||||
this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
|
||||
this.repository = context.getRepository();
|
||||
}
|
||||
|
||||
@Override
|
||||
MergeCommandResult run() throws IOException {
|
||||
MergeResult fastForwardResult = mergeWithFastForwardOnlyMode();
|
||||
if (fastForwardResult.getMergeStatus().isSuccessful()) {
|
||||
push();
|
||||
return createSuccessResult(fastForwardResult.getNewHead().name());
|
||||
MergeCommandResult run() {
|
||||
log.trace("try to fast forward branch {} onto {} in repository {}", request.getBranchToMerge(), request.getTargetBranch(), repository);
|
||||
ObjectId sourceRevision = mergeHelper.getRevisionToMerge();
|
||||
ObjectId targetRevision = mergeHelper.getTargetRevision();
|
||||
|
||||
if (mergeHelper.isMergedInto(targetRevision, sourceRevision)) {
|
||||
log.trace("fast forward branch {} onto {}", request.getBranchToMerge(), request.getTargetBranch());
|
||||
commitHelper.updateBranch(request.getTargetBranch(), sourceRevision, targetRevision);
|
||||
return MergeCommandResult.success(targetRevision.name(), mergeHelper.getRevisionToMerge().name(), sourceRevision.name());
|
||||
} else {
|
||||
log.trace("fast forward is not possible, fallback to merge");
|
||||
return fallbackMerge.run();
|
||||
}
|
||||
}
|
||||
|
||||
private MergeResult mergeWithFastForwardOnlyMode() throws IOException {
|
||||
MergeCommand mergeCommand = getClone().merge();
|
||||
mergeCommand.setFastForward(MergeCommand.FastForwardMode.FF_ONLY);
|
||||
return doMergeInClone(mergeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import sonia.scm.repository.GitChangesetConverter;
|
||||
import sonia.scm.repository.Tag;
|
||||
import sonia.scm.repository.api.HookBranchProvider;
|
||||
@@ -27,17 +28,19 @@ import sonia.scm.repository.api.HookTagProvider;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
class GitImportHookContextProvider extends HookContextProvider {
|
||||
private final GitChangesetConverter converter;
|
||||
private final List<Tag> newTags;
|
||||
private final GitLazyChangesetResolver changesetResolver;
|
||||
private final Supplier<Iterable<RevCommit>> changesetResolver;
|
||||
private final List<String> newBranches;
|
||||
|
||||
GitImportHookContextProvider(GitChangesetConverter converter,
|
||||
List<String> newBranches,
|
||||
List<Tag> newTags,
|
||||
GitLazyChangesetResolver changesetResolver) {
|
||||
Supplier<Iterable<RevCommit>> changesetResolver) {
|
||||
this.converter = converter;
|
||||
this.newTags = newTags;
|
||||
this.changesetResolver = changesetResolver;
|
||||
@@ -81,7 +84,7 @@ class GitImportHookContextProvider extends HookContextProvider {
|
||||
|
||||
@Override
|
||||
public HookChangesetProvider getChangesetProvider() {
|
||||
GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.call(), converter);
|
||||
GitConvertingChangesetIterable changesets = new GitConvertingChangesetIterable(changesetResolver.get(), converter);
|
||||
return r -> new HookChangesetResponse(changesets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
class GitLazyChangesetResolver implements Callable<Iterable<RevCommit>> {
|
||||
class GitLazyChangesetResolver implements Supplier<Iterable<RevCommit>> {
|
||||
private final Repository repository;
|
||||
private final Git git;
|
||||
|
||||
@@ -37,7 +37,7 @@ class GitLazyChangesetResolver implements Callable<Iterable<RevCommit>> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<RevCommit> call() {
|
||||
public Iterable<RevCommit> get() {
|
||||
try {
|
||||
return git.log().all().call();
|
||||
} catch (IOException | GitAPIException e) {
|
||||
|
||||
@@ -31,7 +31,6 @@ import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.GitChangesetConverter;
|
||||
import sonia.scm.repository.GitChangesetConverterFactory;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -40,10 +39,9 @@ import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
|
||||
public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
{
|
||||
public class GitLogCommand extends AbstractGitCommand implements LogCommand {
|
||||
|
||||
|
||||
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GitLogCommand.class);
|
||||
public static final String REVISION = "Revision";
|
||||
@@ -51,20 +49,16 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
|
||||
|
||||
@Inject
|
||||
GitLogCommand(@Assisted GitContext context, GitChangesetConverterFactory converterFactory)
|
||||
{
|
||||
GitLogCommand(@Assisted GitContext context, GitChangesetConverterFactory converterFactory) {
|
||||
super(context);
|
||||
this.converterFactory = converterFactory;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("java:S2093")
|
||||
public Changeset getChangeset(String revision, LogCommandRequest request)
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
public Changeset getChangeset(String revision, LogCommandRequest request) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("fetch changeset {}", revision);
|
||||
}
|
||||
|
||||
@@ -73,18 +67,15 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
GitChangesetConverter converter = null;
|
||||
RevWalk revWalk = null;
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
gr = open();
|
||||
|
||||
if (!gr.getAllRefs().isEmpty())
|
||||
{
|
||||
if (!gr.getAllRefs().isEmpty()) {
|
||||
revWalk = new RevWalk(gr);
|
||||
ObjectId id = GitUtil.getRevisionId(gr, revision);
|
||||
RevCommit commit = revWalk.parseCommit(id);
|
||||
|
||||
if (commit != null)
|
||||
{
|
||||
if (commit != null) {
|
||||
converter = converterFactory.create(gr, revWalk);
|
||||
|
||||
if (isBranchRequested(request)) {
|
||||
@@ -98,23 +89,15 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
} else {
|
||||
changeset = converter.createChangeset(commit);
|
||||
}
|
||||
}
|
||||
else if (logger.isWarnEnabled())
|
||||
{
|
||||
} else if (logger.isWarnEnabled()) {
|
||||
logger.warn("could not find revision {}", revision);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
} catch (IOException ex) {
|
||||
logger.error("could not open repository: " + repository.getNamespaceAndName(), ex);
|
||||
}
|
||||
catch (NullPointerException e)
|
||||
{
|
||||
} catch (NullPointerException e) {
|
||||
throw notFound(entity(REVISION, revision).in(this.repository));
|
||||
}
|
||||
finally
|
||||
{
|
||||
} finally {
|
||||
IOUtil.close(converter);
|
||||
GitUtil.release(revWalk);
|
||||
}
|
||||
@@ -138,14 +121,10 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
|
||||
@Override
|
||||
@SuppressWarnings("java:S2093")
|
||||
public ChangesetPagingResult getChangesets(LogCommandRequest request) {
|
||||
try {
|
||||
if (Strings.isNullOrEmpty(request.getBranch())) {
|
||||
request.setBranch(context.getConfig().getDefaultBranch());
|
||||
}
|
||||
return new GitLogComputer(this.repository.getId(), open(), converterFactory).compute(request);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(repository, "could not create change log", e);
|
||||
if (Strings.isNullOrEmpty(request.getBranch())) {
|
||||
request.setBranch(context.getConfig().getDefaultBranch());
|
||||
}
|
||||
return new GitLogComputer(this.repository.getId(), open(), converterFactory).compute(request);
|
||||
}
|
||||
|
||||
public interface Factory {
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.GitWorkingCopyFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||
import sonia.scm.repository.api.MergePreventReason;
|
||||
@@ -56,6 +57,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
|
||||
private final GitWorkingCopyFactory workingCopyFactory;
|
||||
private final AttributeAnalyzer attributeAnalyzer;
|
||||
private final RepositoryManager repositoryManager;
|
||||
private final GitRepositoryHookEventFactory eventFactory;
|
||||
|
||||
private static final Set<MergeStrategy> STRATEGIES = Set.of(
|
||||
MergeStrategy.MERGE_COMMIT,
|
||||
MergeStrategy.FAST_FORWARD_IF_POSSIBLE,
|
||||
@@ -64,14 +68,24 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
);
|
||||
|
||||
@Inject
|
||||
GitMergeCommand(@Assisted GitContext context, GitRepositoryHandler handler, AttributeAnalyzer attributeAnalyzer) {
|
||||
this(context, handler.getWorkingCopyFactory(), attributeAnalyzer);
|
||||
GitMergeCommand(@Assisted GitContext context,
|
||||
GitRepositoryHandler handler,
|
||||
AttributeAnalyzer attributeAnalyzer,
|
||||
RepositoryManager repositoryManager,
|
||||
GitRepositoryHookEventFactory eventFactory) {
|
||||
this(context, handler.getWorkingCopyFactory(), attributeAnalyzer, repositoryManager, eventFactory);
|
||||
}
|
||||
|
||||
GitMergeCommand(@Assisted GitContext context, GitWorkingCopyFactory workingCopyFactory, AttributeAnalyzer attributeAnalyzer) {
|
||||
GitMergeCommand(@Assisted GitContext context,
|
||||
GitWorkingCopyFactory workingCopyFactory,
|
||||
AttributeAnalyzer attributeAnalyzer,
|
||||
RepositoryManager repositoryManager,
|
||||
GitRepositoryHookEventFactory eventFactory) {
|
||||
super(context);
|
||||
this.workingCopyFactory = workingCopyFactory;
|
||||
this.attributeAnalyzer = attributeAnalyzer;
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.eventFactory = eventFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,22 +99,14 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
}
|
||||
|
||||
private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) {
|
||||
switch (request.getMergeStrategy()) {
|
||||
case SQUASH:
|
||||
return inClone(clone -> new GitMergeWithSquash(clone, request, context, repository), workingCopyFactory, request.getTargetBranch());
|
||||
|
||||
case FAST_FORWARD_IF_POSSIBLE:
|
||||
return inClone(clone -> new GitFastForwardIfPossible(clone, request, context, repository), workingCopyFactory, request.getTargetBranch());
|
||||
|
||||
case MERGE_COMMIT:
|
||||
return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workingCopyFactory, request.getTargetBranch());
|
||||
|
||||
case REBASE:
|
||||
return inClone(clone -> new GitMergeRebase(clone, request, context, repository), workingCopyFactory, request.getTargetBranch());
|
||||
|
||||
default:
|
||||
throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy());
|
||||
}
|
||||
return switch (request.getMergeStrategy()) {
|
||||
case SQUASH -> new GitMergeWithSquash(request, context, repositoryManager, eventFactory).run();
|
||||
case FAST_FORWARD_IF_POSSIBLE ->
|
||||
new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory).run();
|
||||
case MERGE_COMMIT -> new GitMergeCommit(request, context, repositoryManager, eventFactory).run();
|
||||
case REBASE -> new GitMergeRebase(request, context, repositoryManager, eventFactory).run();
|
||||
default -> throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,39 +16,21 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.MergeCommand;
|
||||
import org.eclipse.jgit.api.MergeResult;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import sonia.scm.NoChangesMadeException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
class GitMergeCommit {
|
||||
|
||||
import static sonia.scm.repository.spi.GitRevisionExtractor.extractRevisionFromRevCommit;
|
||||
private final MergeCommandRequest request;
|
||||
private final MergeHelper mergeHelper;
|
||||
|
||||
class GitMergeCommit extends GitMergeStrategy {
|
||||
|
||||
GitMergeCommit(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
|
||||
super(clone, request, context, repository);
|
||||
GitMergeCommit(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
|
||||
this.request = request;
|
||||
this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
MergeCommandResult run() throws IOException {
|
||||
MergeCommand mergeCommand = getClone().merge();
|
||||
mergeCommand.setFastForward(MergeCommand.FastForwardMode.NO_FF);
|
||||
MergeResult result = doMergeInClone(mergeCommand);
|
||||
|
||||
if (result.getMergeStatus().isSuccessful()) {
|
||||
RevCommit revCommit = doCommit().orElseThrow(() -> new NoChangesMadeException(getRepository()));
|
||||
push();
|
||||
return createSuccessResult(extractRevisionFromRevCommit(revCommit));
|
||||
} else {
|
||||
return analyseFailure(result);
|
||||
}
|
||||
MergeCommandResult run() {
|
||||
return mergeHelper.doRecursiveMerge(request, (sourceRevision, targetRevision) -> new ObjectId[]{targetRevision, sourceRevision});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,73 +16,108 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.MergeCommand;
|
||||
import org.eclipse.jgit.api.RebaseResult;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jgit.api.errors.CanceledException;
|
||||
import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.merge.ResolveMerger;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevSort;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class GitMergeRebase extends GitMergeStrategy {
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.eclipse.jgit.merge.MergeStrategy.RESOLVE;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GitMergeRebase.class);
|
||||
@Slf4j
|
||||
class GitMergeRebase {
|
||||
|
||||
private final MergeCommandRequest request;
|
||||
private final GitContext context;
|
||||
private final MergeHelper mergeHelper;
|
||||
private final CommitHelper commitHelper;
|
||||
private final GitFastForwardIfPossible fastForwardMerge;
|
||||
|
||||
GitMergeRebase(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
|
||||
super(clone, request, context, repository);
|
||||
GitMergeRebase(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
|
||||
this.request = request;
|
||||
this.context = context;
|
||||
this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
|
||||
this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
|
||||
this.fastForwardMerge = new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
MergeCommandResult run() throws IOException {
|
||||
RebaseResult result;
|
||||
String branchToMerge = request.getBranchToMerge();
|
||||
String targetBranch = request.getTargetBranch();
|
||||
try {
|
||||
checkOutBranch(branchToMerge);
|
||||
result =
|
||||
getClone()
|
||||
.rebase()
|
||||
.setUpstream(targetBranch)
|
||||
.call();
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(getContext().getRepository(), "could not rebase branch " + branchToMerge + " onto " + targetBranch, e);
|
||||
MergeCommandResult run() {
|
||||
log.debug("rebase branch {} onto {}", request.getBranchToMerge(), request.getTargetBranch());
|
||||
|
||||
ObjectId sourceRevision = mergeHelper.getRevisionToMerge();
|
||||
ObjectId targetRevision = mergeHelper.getTargetRevision();
|
||||
if (mergeHelper.isMergedInto(targetRevision, sourceRevision)) {
|
||||
log.trace("fast forward is possible; using fast forward merge");
|
||||
return fastForwardMerge.run();
|
||||
}
|
||||
|
||||
if (result.getStatus().isSuccessful()) {
|
||||
return fastForwardTargetBranch(branchToMerge, targetBranch, result);
|
||||
} else {
|
||||
logger.info("could not rebase branch {} into {} with rebase status '{}' due to ...", branchToMerge, targetBranch, result.getStatus());
|
||||
logger.info("... conflicts: {}", result.getConflicts());
|
||||
logger.info("... failing paths: {}", result.getFailingPaths());
|
||||
logger.info("... message: {}", result);
|
||||
return MergeCommandResult.failure(branchToMerge, targetBranch, Optional.ofNullable(result.getConflicts()).orElse(Collections.singletonList("UNKNOWN")));
|
||||
try {
|
||||
List<RevCommit> commits = computeCommits();
|
||||
Collections.reverse(commits);
|
||||
|
||||
for (RevCommit commit : commits) {
|
||||
log.trace("rebase {} onto {}", commit, targetRevision);
|
||||
ResolveMerger merger = (ResolveMerger) RESOLVE.newMerger(context.open(), true); // The recursive merger is always a RecursiveMerge
|
||||
merger.setBase(commit.getParent(0));
|
||||
boolean mergeSucceeded = merger.merge(commit, targetRevision);
|
||||
if (!mergeSucceeded) {
|
||||
log.trace("could not merge {} into {}", commit, targetRevision);
|
||||
return MergeCommandResult.failure(request.getBranchToMerge(), request.getTargetBranch(), ofNullable(merger.getUnmergedPaths()).orElse(Collections.singletonList("UNKNOWN")));
|
||||
}
|
||||
ObjectId newTreeId = merger.getResultTreeId();
|
||||
log.trace("create commit for new tree {}", newTreeId);
|
||||
|
||||
PersonIdent originalAuthor = commit.getAuthorIdent();
|
||||
targetRevision = commitHelper.createCommit(
|
||||
newTreeId,
|
||||
originalAuthor,
|
||||
request.getAuthor(),
|
||||
commit.getFullMessage(),
|
||||
request.isSign(),
|
||||
targetRevision
|
||||
);
|
||||
log.trace("created {}", targetRevision);
|
||||
}
|
||||
log.trace("update branch {} to new revision {}", request.getTargetBranch(), targetRevision);
|
||||
commitHelper.updateBranch(request.getTargetBranch(), targetRevision, mergeHelper.getTargetRevision());
|
||||
return MergeCommandResult.success(targetRevision.name(), mergeHelper.getRevisionToMerge().name(), targetRevision.name());
|
||||
} catch (IOException | CanceledException | UnsupportedSigningFormatException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not rebase branch " + request.getBranchToMerge() + " onto " + request.getTargetBranch(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private MergeCommandResult fastForwardTargetBranch(String branchToMerge, String targetBranch, RebaseResult result) throws IOException {
|
||||
try {
|
||||
getClone().checkout().setName(targetBranch).call();
|
||||
ObjectId sourceRevision = resolveRevision(branchToMerge);
|
||||
getClone()
|
||||
.merge()
|
||||
.setFastForward(MergeCommand.FastForwardMode.FF_ONLY)
|
||||
.include(branchToMerge, sourceRevision)
|
||||
.call();
|
||||
push();
|
||||
return createSuccessResult(sourceRevision.name());
|
||||
} catch (GitAPIException e) {
|
||||
return MergeCommandResult.failure(branchToMerge, targetBranch, result.getConflicts());
|
||||
}
|
||||
private List<RevCommit> computeCommits() throws IOException {
|
||||
List<RevCommit> cherryPickList = new ArrayList<>();
|
||||
try (RevWalk revWalk = new RevWalk(context.open())) {
|
||||
revWalk.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true);
|
||||
revWalk.sort(RevSort.COMMIT_TIME_DESC, true);
|
||||
revWalk.markUninteresting(revWalk.lookupCommit(mergeHelper.getTargetRevision()));
|
||||
revWalk.markStart(revWalk.lookupCommit(mergeHelper.getRevisionToMerge()));
|
||||
|
||||
for (RevCommit commit : revWalk) {
|
||||
if (commit.getParentCount() <= 1) {
|
||||
log.trace("add {} to cherry pick list", commit);
|
||||
cherryPickList.add(commit);
|
||||
} else {
|
||||
log.trace("skip {} because it has more than one parent", commit);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cherryPickList;
|
||||
}
|
||||
}
|
||||
|
||||