Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use the new GitRemote logic - Support multiple URIs per SCM server #94

Merged
merged 11 commits into from
Sep 2, 2024
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,31 @@ file is included in this repository.

### Mapping repositories

Edit [application.yaml](src/main/resources/application.yaml) to list all the origins (host + context path) for the SCM providers listed in the repos.csv, and provide a type: `[GITHUB,GITLAB,BITBUCKET_CLOUD,BITBUCKET,AZURE_DEVOPS]`.
This will be used to split the clone url into an origin and path.
Edit [application.yaml](src/main/resources/application.yaml) and list all the base uris for any private SCM providers used in the repos.csv.

You will need to provide the base uri, type and any alternative uri that can be used to access the same SCM server.
pstreef marked this conversation as resolved.
Show resolved Hide resolved

Example:
```yaml
moderne:
scm:
repositories:
- baseUri: https://bitbucket.example.com/stash
pstreef marked this conversation as resolved.
Show resolved Hide resolved
type: Bitbucket
alternativeUris:
- http://bitbucket.example.com:8080/stash
- ssh://bitbucket.example.com/stash
- ssh://bitbucket.example.com:7999/stash
```

Make sure to provide the service type. The following (self-hosted) SCM providers are supported: `[GitHub,GitLab,Bitbucket]`.

Note that for an on-premise Bitbucket (server) we should not have the `scm/` path segment in the origin or the path. This will automatically be stripped off if you configure this as explained above.
We use this configuration to split the clone uri into an origin and path.
pstreef marked this conversation as resolved.
Show resolved Hide resolved

Note that for an on-premise Bitbucket (DC/server) we should not have the `scm/` path segment in the origin or the path. This will automatically be stripped off if you configure this as explained above.

Example:
If you have a repository at `cloneUrl=https://bitbucket.example.com/stash/scm/openrewrite/rewrite.git` and supply `bitbucket.example.com/stash` it will create a repository:
If you have a repository at `cloneUrl=https://bitbucket.example.com/stash/scm/openrewrite/rewrite.git` and supply `https://bitbucket.example.com/stash` as the origin it will create a repository:

```
{
Expand All @@ -77,6 +95,8 @@ If you have a repository at `cloneUrl=https://bitbucket.example.com/stash/scm/op

you can set `moderne.scm.allow-missing-scm-origins` in [application.yaml](src/main/resources/application.yaml) to true if you want to strictly check on startup that all origins in repos.csv are present.

Note: for backwards compatibility we read the `origin` if the `baseUri` is not supplied and apply the default `https://` protocol to it. It is highly recommended to update the configuration to contain full URIs.

### Commit options
The `commitOptions` field on the `Organization` type is a list of strings that represent the commit options that are
available to developers in that organization. These are the options that are presented to developers when they create a
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/io/moderne/organizations/Application.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.moderne.organizations;

import org.openrewrite.GitRemote;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@EnableConfigurationProperties(ScmConfiguration.class)
@SpringBootApplication
Expand All @@ -12,4 +14,11 @@ public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
GitRemote.Parser gitRemoteParser(ScmConfiguration scmConfiguration) {
GitRemote.Parser gitRemoteParser = new GitRemote.Parser();
scmConfiguration.getRepositories().forEach(scmRepository -> gitRemoteParser.registerRemote(scmRepository.getType(), scmRepository.getBaseUri(), scmRepository.getAlternativeUris()));
return gitRemoteParser;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import io.moderne.organizations.types.CommitOption;
import io.moderne.organizations.types.RepositoryInput;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.GitRemote;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
Expand All @@ -11,11 +11,7 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* This service could also directly call your SCM api to determine the available repositories
Expand All @@ -25,12 +21,13 @@ public class OrganizationStructureService {
private static final String REPOS_CSV = "repos.csv";
private static final String NAME_MAPPING = "id-mapping.txt";
private static final Logger log = LoggerFactory.getLogger(OrganizationStructureService.class.getName());

private final ScmConfiguration scmConfiguration;
private final RepositoryMapper repositoryMapper;
private final GitRemote.Parser gitRemoteParser;

public OrganizationStructureService(ScmConfiguration scmConfiguration) {
OrganizationStructureService(ScmConfiguration scmConfiguration, GitRemote.Parser gitRemoteParser) {
this.scmConfiguration = scmConfiguration;
repositoryMapper = new RepositoryMapper(scmConfiguration);
this.gitRemoteParser = gitRemoteParser;
}

public Map<String, OrganizationRepositories> readOrganizationStructure() {
Expand All @@ -49,7 +46,7 @@ public Map<String, OrganizationRepositories> readOrganizationStructure() {
}
String cloneUrl = fields[0].trim();
String branch = fields[1].trim();
RepositoryInput repository = repositoryMapper.determineRepository(cloneUrl, branch);
RepositoryInput repository = determineRepository(cloneUrl, branch);
if (repository == null) {
if (scmConfiguration.isAllowMissingScmOrigins()) {
log.warn("No scm origin found for %s. Consider adding it to scm-origins.txt".formatted(cloneUrl));
Expand Down Expand Up @@ -137,62 +134,8 @@ public static void printTree(Map<String, List<OrganizationRepositories>> tree, S
}
}

private static class RepositoryMapper {
Map<ScmConfiguration.ScmRepository, Pattern> urlPatterns = new LinkedHashMap<>();

private RepositoryMapper(ScmConfiguration scmConfiguration) {
for (ScmConfiguration.ScmRepository repository : scmConfiguration.getRepositories()) {
Pattern pattern = Pattern.compile(repository.getOrigin() + "(.*)");
urlPatterns.put(repository, pattern);
}
}

@Nullable
public RepositoryInput determineRepository(String cloneUrl, String branch) {
for (Map.Entry<ScmConfiguration.ScmRepository, Pattern> entry : urlPatterns.entrySet()) {
String origin = cleanOrigin(entry.getKey().getOrigin());
Matcher matcher = entry.getValue().matcher(cloneUrl);
if (matcher.find()) {
String path = cleanPath(matcher.group(1), entry.getKey().getType());
return new RepositoryInput(path, origin, branch);
}
}
return null;
}

private static String cleanOrigin(String origin) {
if (origin.startsWith("http://") || origin.startsWith("https://") || origin.startsWith("ssh://")) {
try {
final URL url = new URL(origin);
origin = url.getHost();
if (url.getPort() != -1) {
origin += ":" + url.getPort();
}
origin += url.getPath();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
if (origin.endsWith("/")) {
origin = origin.substring(0, origin.length() - 1);
}
return origin;
}

private static String cleanPath(String path, ScmConfiguration.ScmRepository.Type type) {
if (path.startsWith("/")) {
path = path.substring(1);
}
// In case of bitbucket server/on prem we need to remove the `/scm` prefix.
// This prefix is not part of all URL's to repository resource
// (for instance pull requests) so it cannot be part of the origin or path.
if (type == ScmConfiguration.ScmRepository.Type.BITBUCKET && path.startsWith("scm/")) {
path = path.substring(4);
}
if (path.endsWith(".git")) {
path = path.substring(0, path.length() - 4);
}
return path;
}
RepositoryInput determineRepository(String cloneUrl, String branch) {
GitRemote gitRemote = gitRemoteParser.parse(cloneUrl);
return new RepositoryInput(gitRemote.getOrigin(), gitRemote.getPath(), branch);
}
}
106 changes: 74 additions & 32 deletions src/main/java/io/moderne/organizations/ScmConfiguration.java
Original file line number Diff line number Diff line change
@@ -1,44 +1,20 @@
package io.moderne.organizations;

import org.jspecify.annotations.Nullable;
import org.openrewrite.GitRemote;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.ArrayList;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

@ConfigurationProperties(prefix = "moderne.scm")
public class ScmConfiguration {
private List<ScmRepository> repositories;
class ScmConfiguration {
private boolean allowMissingScmOrigins;

public static class ScmRepository {
String origin;
Type type;

public String getOrigin() {
return origin;
}

public void setOrigin(String origin) {
this.origin = origin;
}

public Type getType() {
return type;
}

public void setType(Type type) {
this.type = type;
}

public enum Type {
GITHUB, BITBUCKET_CLOUD, GITLAB, BITBUCKET, AZURE_DEVOPS
}
}
private List<ScmRepository> repositories;

public List<ScmRepository> getRepositories() {
if (repositories == null) {
repositories = new ArrayList<>();
}
return repositories;
}

Expand All @@ -54,5 +30,71 @@ public void setAllowMissingScmOrigins(boolean allowMissingScmOrigins) {
this.allowMissingScmOrigins = allowMissingScmOrigins;
}

static class ScmRepository {
@Deprecated
pstreef marked this conversation as resolved.
Show resolved Hide resolved
@Nullable
private String origin;

private GitRemote.Service type;
pstreef marked this conversation as resolved.
Show resolved Hide resolved

@Nullable
private URI baseUri;

@Nullable
private List<URI> alternativeUris;

public List<URI> getAlternativeUris() {
if (baseUri != null) {
return Objects.requireNonNullElse(alternativeUris, Collections.emptyList());
}
{
pstreef marked this conversation as resolved.
Show resolved Hide resolved
// backwards compatibility
if (alternativeUris != null) {
throw new IllegalStateException("Using alternativeUris without baseUri is not supported");
}
if (originHasProtocol()) {
return Collections.emptyList();
}
return Collections.singletonList(URI.create("ssh://" + origin));
pstreef marked this conversation as resolved.
Show resolved Hide resolved
}
}

public URI getBaseUri() {
if (baseUri != null) {
return baseUri;
}
{
pstreef marked this conversation as resolved.
Show resolved Hide resolved
// backwards compatibility
if (origin == null) {
throw new IllegalStateException("Either baseUri or origin must be set");
}
if (originHasProtocol()) {
return URI.create(origin);
}
return URI.create("https://" + origin);
}
}

private boolean originHasProtocol() {
return origin != null && (origin.startsWith("ssh://") || origin.startsWith("https:/") || origin.startsWith("http://"));
}

public void setOrigin(@Nullable String origin) {
this.origin = origin;
}

public GitRemote.Service getType() {
return type;
}

public void setType(GitRemote.Service type) {
this.type = type;
}

public void setAlternativeUris(@Nullable List<URI> alternativeUris) {
this.alternativeUris = alternativeUris;
}
}


}
}
14 changes: 5 additions & 9 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ moderne:
scm:
allowMissingScmOrigins: true
repositories:
- origin: github.com
type: GITHUB
- origin: bitbucket.org
type: BITBUCKET_CLOUD
- origin: gitlab.com
type: GITLAB
- origin: https://bitbucket.example.com/stash
type: BITBUCKET
- origin: dev.azure.com
type: AZURE_DEVOPS
type: Bitbucket
alternative-uris:
pstreef marked this conversation as resolved.
Show resolved Hide resolved
- http://bitbucket.example.com:8080/stash
- ssh://bitbucket.example.com/stash
- ssh://bitbucket.example.com:7999/stash
Loading