feat(java): add support remote repositories from settings.xml files (#9708)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
DmitriyLewen
2025-10-28 14:35:19 +06:00
committed by GitHub
parent fb0593bee6
commit eff52eb2e6
10 changed files with 715 additions and 154 deletions

View File

@@ -29,14 +29,10 @@ import (
xio "github.com/aquasecurity/trivy/pkg/x/io"
)
const (
centralURL = "https://repo.maven.apache.org/maven2/"
)
type options struct {
offline bool
releaseRemoteRepos []string
snapshotRemoteRepos []string
offline bool
defaultRepo repository
settingsRepos []repository
}
type option func(*options)
@@ -47,55 +43,71 @@ func WithOffline(offline bool) option {
}
}
func WithReleaseRemoteRepos(repos []string) option {
func WithDefaultRepo(repoURL string, releaseEnabled, snapshotEnabled bool) option {
return func(opts *options) {
opts.releaseRemoteRepos = repos
u, _ := url.Parse(repoURL)
opts.defaultRepo = repository{
url: *u,
releaseEnabled: releaseEnabled,
snapshotEnabled: snapshotEnabled,
}
}
}
func WithSnapshotRemoteRepos(repos []string) option {
func WithSettingsRepos(repoURLs []string, releaseEnabled, snapshotEnabled bool) option {
return func(opts *options) {
opts.snapshotRemoteRepos = repos
opts.settingsRepos = lo.Map(repoURLs, func(repoURL string, _ int) repository {
u, _ := url.Parse(repoURL)
return repository{
url: *u,
releaseEnabled: releaseEnabled,
snapshotEnabled: snapshotEnabled,
}
})
}
}
type Parser struct {
logger *log.Logger
rootPath string
cache pomCache
localRepository string
releaseRemoteRepos []string
snapshotRemoteRepos []string
offline bool
servers []Server
logger *log.Logger
rootPath string
cache pomCache
localRepository string
remoteRepos repositories
offline bool
servers []Server
}
func NewParser(filePath string, opts ...option) *Parser {
o := &options{
offline: false,
releaseRemoteRepos: []string{centralURL}, // Maven doesn't use central repository for snapshot dependencies
}
for _, opt := range opts {
opt(o)
offline: false,
defaultRepo: mavenCentralRepo,
}
s := readSettings()
o.settingsRepos = s.effectiveRepositories()
localRepository := s.LocalRepository
if localRepository == "" {
homeDir, _ := os.UserHomeDir()
localRepository = filepath.Join(homeDir, ".m2", "repository")
}
for _, opt := range opts {
opt(o)
}
remoteRepos := repositories{
defaultRepo: o.defaultRepo,
settings: o.settingsRepos,
}
return &Parser{
logger: log.WithPrefix("pom"),
rootPath: filepath.Clean(filePath),
cache: newPOMCache(),
localRepository: localRepository,
releaseRemoteRepos: o.releaseRemoteRepos,
snapshotRemoteRepos: o.snapshotRemoteRepos,
offline: o.offline,
servers: s.Servers,
logger: log.WithPrefix("pom"),
rootPath: filepath.Clean(filePath),
cache: newPOMCache(),
localRepository: localRepository,
remoteRepos: remoteRepos,
offline: o.offline,
servers: s.Servers,
}
}
@@ -362,9 +374,10 @@ func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (a
opts.exclusions = set.New[string]()
}
// Update remoteRepositories
pomReleaseRemoteRepos, pomSnapshotRemoteRepos := pom.repositories(p.servers)
p.releaseRemoteRepos = lo.Uniq(append(pomReleaseRemoteRepos, p.releaseRemoteRepos...))
p.snapshotRemoteRepos = lo.Uniq(append(pomSnapshotRemoteRepos, p.snapshotRemoteRepos...))
pomRepos := pom.repositories(p.servers)
p.remoteRepos.pom = lo.UniqBy(append(pomRepos, p.remoteRepos.pom...), func(r repository) url.URL {
return r.url
})
// Resolve parent POM
if err := p.resolveParent(ctx, pom); err != nil {
@@ -710,17 +723,19 @@ func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []str
return nil, xerrors.New("offline mode")
}
remoteRepos := p.releaseRemoteRepos
// Maven uses only snapshot repos for snapshot artifacts
if snapshot {
remoteRepos = p.snapshotRemoteRepos
}
// Try all remoteRepositories by following order:
// 1. remoteRepositories from settings.xml
// 2. remoteRepositories from pom.xml
// 3. default remoteRepository (Maven Central for Release repository)
for _, repo := range slices.Concat(p.remoteRepos.settings, p.remoteRepos.pom, []repository{p.remoteRepos.defaultRepo}) {
// Skip Release only repositories for snapshot artifacts and vice versa
if snapshot && !repo.snapshotEnabled || !snapshot && !repo.releaseEnabled {
continue
}
// try all remoteRepositories
for _, repo := range remoteRepos {
repoPaths := slices.Clone(paths) // Clone slice to avoid overwriting last element of `paths`
if snapshot {
pomFileName, err := p.fetchPomFileNameFromMavenMetadata(ctx, repo, repoPaths)
pomFileName, err := p.fetchPomFileNameFromMavenMetadata(ctx, repo.url, repoPaths)
if err != nil {
return nil, xerrors.Errorf("fetch maven-metadata.xml error: %w", err)
}
@@ -729,7 +744,7 @@ func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []str
repoPaths[len(repoPaths)-1] = pomFileName
}
}
fetched, err := p.fetchPOMFromRemoteRepository(ctx, repo, repoPaths)
fetched, err := p.fetchPOMFromRemoteRepository(ctx, repo.url, repoPaths)
if err != nil {
return nil, xerrors.Errorf("fetch repository error: %w", err)
} else if fetched == nil {
@@ -740,12 +755,7 @@ func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []str
return nil, xerrors.Errorf("the POM was not found in remote remoteRepositories")
}
func (p *Parser) remoteRepoRequest(ctx context.Context, repo string, paths []string) (*http.Request, error) {
repoURL, err := url.Parse(repo)
if err != nil {
return nil, xerrors.Errorf("unable to parse URL: %w", err)
}
func (p *Parser) remoteRepoRequest(ctx context.Context, repoURL url.URL, paths []string) (*http.Request, error) {
paths = append([]string{repoURL.Path}, paths...)
repoURL.Path = path.Join(paths...)
@@ -762,14 +772,14 @@ func (p *Parser) remoteRepoRequest(ctx context.Context, repo string, paths []str
}
// fetchPomFileNameFromMavenMetadata fetches `maven-metadata.xml` file to detect file name of pom file.
func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repo string, paths []string) (string, error) {
func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repoURL url.URL, paths []string) (string, error) {
// Overwrite pom file name to `maven-metadata.xml`
mavenMetadataPaths := slices.Clone(paths[:len(paths)-1]) // Clone slice to avoid shadow overwriting last element of `paths`
mavenMetadataPaths = append(mavenMetadataPaths, "maven-metadata.xml")
req, err := p.remoteRepoRequest(ctx, repo, mavenMetadataPaths)
req, err := p.remoteRepoRequest(ctx, repoURL, mavenMetadataPaths)
if err != nil {
p.logger.Debug("Unable to create request", log.String("repo", repo), log.Err(err))
p.logger.Debug("Unable to create request", log.String("repo", repoURL.Redacted()), log.Err(err))
return "", nil
}
@@ -779,14 +789,16 @@ func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repo str
if shouldReturnError(err) {
return "", err
}
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Err(err))
return "", nil
} else if resp.StatusCode != http.StatusOK {
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Int("statusCode", resp.StatusCode))
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Err(err))
return "", nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Int("statusCode", resp.StatusCode))
return "", nil
}
mavenMetadata, err := parseMavenMetadata(resp.Body)
if err != nil {
return "", xerrors.Errorf("failed to parse maven-metadata.xml file: %w", err)
@@ -803,10 +815,10 @@ func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repo str
return pomFileName, nil
}
func (p *Parser) fetchPOMFromRemoteRepository(ctx context.Context, repo string, paths []string) (*pom, error) {
req, err := p.remoteRepoRequest(ctx, repo, paths)
func (p *Parser) fetchPOMFromRemoteRepository(ctx context.Context, repoURL url.URL, paths []string) (*pom, error) {
req, err := p.remoteRepoRequest(ctx, repoURL, paths)
if err != nil {
p.logger.Debug("Unable to create request", log.String("repo", repo), log.Err(err))
p.logger.Debug("Unable to create request", log.String("repo", repoURL.Redacted()), log.Err(err))
return nil, nil
}
@@ -816,14 +828,16 @@ func (p *Parser) fetchPOMFromRemoteRepository(ctx context.Context, repo string,
if shouldReturnError(err) {
return nil, err
}
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Err(err))
return nil, nil
} else if resp.StatusCode != http.StatusOK {
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Int("statusCode", resp.StatusCode))
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Err(err))
return nil, nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Int("statusCode", resp.StatusCode))
return nil, nil
}
content, err := parsePom(resp.Body, false)
if err != nil {
return nil, xerrors.Errorf("failed to parse the remote POM: %w", err)

View File

@@ -110,13 +110,14 @@ var (
func TestPom_Parse(t *testing.T) {
tests := []struct {
name string
inputFile string
local bool
offline bool
want []ftypes.Package
wantDeps []ftypes.Dependency
wantErr string
name string
inputFile string
local bool
enableRepoForSettingsRepo bool // use another repo for repository from settings.xml
offline bool
want []ftypes.Package
wantDeps []ftypes.Dependency
wantErr string
}{
{
name: "local repository",
@@ -326,6 +327,55 @@ func TestPom_Parse(t *testing.T) {
},
},
},
{
name: "multiple repositories are used",
inputFile: filepath.Join("testdata", "happy", "pom.xml"),
local: false,
enableRepoForSettingsRepo: true,
want: []ftypes.Package{
{
ID: "com.example:happy:1.0.0",
Name: "com.example:happy",
Version: "1.0.0",
Licenses: []string{"BSD-3-Clause"},
Relationship: ftypes.RelationshipRoot,
},
{
ID: "org.example:example-api:1.7.30",
Name: "org.example:example-api",
Version: "1.7.30",
Licenses: []string{"Custom License from custom repo"},
Relationship: ftypes.RelationshipDirect,
Locations: ftypes.Locations{
{
StartLine: 32,
EndLine: 36,
},
},
},
{
ID: "org.example:example-runtime:1.0.0",
Name: "org.example:example-runtime",
Version: "1.0.0",
Relationship: ftypes.RelationshipDirect,
Locations: ftypes.Locations{
{
StartLine: 37,
EndLine: 42,
},
},
},
},
wantDeps: []ftypes.Dependency{
{
ID: "com.example:happy:1.0.0",
DependsOn: []string{
"org.example:example-api:1.7.30",
"org.example:example-runtime:1.0.0",
},
},
},
},
{
name: "inherit parent properties",
inputFile: filepath.Join("testdata", "parent-properties", "child", "pom.xml"),
@@ -2206,7 +2256,8 @@ func TestPom_Parse(t *testing.T) {
require.NoError(t, err)
defer f.Close()
var remoteRepos []string
var defaultRepo string
var settingsRepos []string
if tt.local {
// for local repository
t.Setenv("MAVEN_HOME", "testdata/settings/global")
@@ -2214,10 +2265,18 @@ func TestPom_Parse(t *testing.T) {
// for remote repository
h := http.FileServer(http.Dir(filepath.Join("testdata", "repository")))
ts := httptest.NewServer(h)
remoteRepos = []string{ts.URL}
defaultRepo = ts.URL
// Enable custom repository to be sure in repository order checking
if tt.enableRepoForSettingsRepo {
ch := http.FileServer(http.Dir(filepath.Join("testdata", "repository-for-settings-repo")))
cts := httptest.NewServer(ch)
settingsRepos = []string{cts.URL}
}
}
p := pom.NewParser(tt.inputFile, pom.WithReleaseRemoteRepos(remoteRepos), pom.WithSnapshotRemoteRepos(remoteRepos), pom.WithOffline(tt.offline))
p := pom.NewParser(tt.inputFile, pom.WithDefaultRepo(defaultRepo, true, true),
pom.WithSettingsRepos(settingsRepos, true, false), pom.WithOffline(tt.offline))
gotPkgs, gotDeps, err := p.Parse(t.Context(), f)
if tt.wantErr != "" {

View File

@@ -4,7 +4,6 @@ import (
"encoding/xml"
"fmt"
"io"
"net/url"
"reflect"
"strings"
@@ -13,7 +12,6 @@ import (
"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/set"
"github.com/aquasecurity/trivy/pkg/x/slices"
)
@@ -123,42 +121,8 @@ func (p *pom) licenses() []string {
}))
}
func (p *pom) repositories(servers []Server) ([]string, []string) {
logger := log.WithPrefix("pom")
var releaseRepos, snapshotRepos []string
for _, rep := range p.content.Repositories.Repository {
snapshot := rep.Snapshots.Enabled == "true"
release := rep.Releases.Enabled == "true"
// Add only enabled repositories
if !release && !snapshot {
continue
}
repoURL, err := url.Parse(rep.URL)
if err != nil {
logger.Debug("Unable to parse remote repository url", log.Err(err))
continue
}
// Get the credentials from settings.xml based on matching server id
// with the repository id from pom.xml and use it for accessing the repository url
for _, server := range servers {
if rep.ID == server.ID && server.Username != "" && server.Password != "" {
repoURL.User = url.UserPassword(server.Username, server.Password)
break
}
}
logger.Debug("Adding repository", log.String("id", rep.ID), log.String("url", rep.URL))
if snapshot {
snapshotRepos = append(snapshotRepos, repoURL.String())
}
if release {
releaseRepos = append(releaseRepos, repoURL.String())
}
}
return releaseRepos, snapshotRepos
func (p *pom) repositories(servers []Server) []repository {
return resolvePomRepos(servers, p.content.Repositories)
}
type pomXML struct {
@@ -177,7 +141,7 @@ type pomXML struct {
Dependencies pomDependencies `xml:"dependencies"`
} `xml:"dependencyManagement"`
Dependencies pomDependencies `xml:"dependencies"`
Repositories pomRepositories `xml:"repositories"`
Repositories []pomRepository `xml:"repositories>repository"`
}
type pomParent struct {
@@ -384,22 +348,10 @@ func findDep(name string, depManagement []pomDependency) (pomDependency, bool) {
})
}
type pomRepositories struct {
Text string `xml:",chardata"`
Repository []pomRepository `xml:"repository"`
}
type pomRepository struct {
Text string `xml:",chardata"`
ID string `xml:"id"`
Name string `xml:"name"`
URL string `xml:"url"`
Releases struct {
Text string `xml:",chardata"`
Enabled string `xml:"enabled"`
} `xml:"releases"`
Snapshots struct {
Text string `xml:",chardata"`
Enabled string `xml:"enabled"`
} `xml:"snapshots"`
ID string `xml:"id"`
Name string `xml:"name"`
URL string `xml:"url"`
ReleasesEnabled string `xml:"releases>enabled"`
SnapshotsEnabled string `xml:"snapshots>enabled"`
}

View File

@@ -0,0 +1,67 @@
package pom
import (
"errors"
"net/url"
"github.com/aquasecurity/trivy/pkg/log"
)
var centralURL, _ = url.Parse("https://repo.maven.apache.org/maven2/")
type repository struct {
url url.URL
releaseEnabled bool
snapshotEnabled bool
}
type repositories struct {
settings []repository // Repositories from settings.xml files
pom []repository // Repositories from pom file and its parents (parent and upper pom files)
defaultRepo repository // Default repository - Maven Central for Release, empty for Snapshot
}
var mavenCentralRepo = repository{
url: *centralURL,
releaseEnabled: true,
}
func resolvePomRepos(servers []Server, pomRepos []pomRepository) []repository {
logger := log.WithPrefix("pom")
var repos []repository
for _, rep := range pomRepos {
r := repository{
releaseEnabled: rep.ReleasesEnabled == "true",
snapshotEnabled: rep.SnapshotsEnabled == "true",
}
// Add only enabled repositories
if !r.releaseEnabled && !r.snapshotEnabled {
continue
}
repoURL, err := url.Parse(rep.URL)
if err != nil {
var ue *url.Error
if errors.As(err, &ue) {
err = ue.Unwrap()
}
logger.Debug("Unable to parse remote repository url", log.String("id", rep.ID), log.Err(err))
continue
}
// Get the credentials from settings.xml based on matching server id
// with the repository id from pom.xml and use it for accessing the repository url
for _, server := range servers {
if rep.ID == server.ID && server.Username != "" && server.Password != "" {
repoURL.User = url.UserPassword(server.Username, server.Password)
break
}
}
logger.Debug("Adding repository", log.String("id", rep.ID), log.String("url", repoURL.Redacted()))
r.url = *repoURL
repos = append(repos, r)
}
return repos
}

View File

@@ -4,7 +4,10 @@ import (
"encoding/xml"
"os"
"path/filepath"
"slices"
"github.com/samber/lo"
"github.com/samber/lo/mutable"
"golang.org/x/net/html/charset"
)
@@ -14,20 +17,35 @@ type Server struct {
Password string `xml:"password"`
}
type settings struct {
LocalRepository string `xml:"localRepository"`
Servers []Server `xml:"servers>server"`
type Profile struct {
ID string `xml:"id"`
Repositories []pomRepository `xml:"repositories>repository"`
ActiveByDefault bool `xml:"activation>activeByDefault"`
}
// serverFound checks that servers already contain server.
// Maven compares servers by ID only.
func serverFound(servers []Server, id string) bool {
for _, server := range servers {
if server.ID == id {
return true
type settings struct {
LocalRepository string `xml:"localRepository"`
Servers []Server `xml:"servers>server"`
Profiles []Profile `xml:"profiles>profile"`
ActiveProfiles []string `xml:"activeProfiles>activeProfile"`
}
func (s settings) effectiveRepositories() []repository {
var pomRepos []pomRepository
for _, profile := range s.Profiles {
if slices.Contains(s.ActiveProfiles, profile.ID) || profile.ActiveByDefault {
pomRepos = append(pomRepos, profile.Repositories...)
}
}
return false
pomRepos = lo.UniqBy(pomRepos, func(r pomRepository) string {
return r.ID
})
// mvn takes repositories from settings in reverse order
// cf. https://github.com/aquasecurity/trivy/issues/7807#issuecomment-2541485152
mutable.Reverse(pomRepos)
return resolvePomRepos(s.Servers, pomRepos)
}
func readSettings() settings {
@@ -52,12 +70,18 @@ func readSettings() settings {
if s.LocalRepository == "" {
s.LocalRepository = globalSettings.LocalRepository
}
// Maven checks user servers first, then global servers
for _, server := range globalSettings.Servers {
if !serverFound(s.Servers, server.ID) {
s.Servers = append(s.Servers, server)
}
}
// Maven servers
s.Servers = lo.UniqBy(append(s.Servers, globalSettings.Servers...), func(server Server) string {
return server.ID
})
// Merge profiles
s.Profiles = lo.UniqBy(append(s.Profiles, globalSettings.Profiles...), func(p Profile) string {
return p.ID
})
// Merge active profiles
s.ActiveProfiles = lo.Uniq(append(s.ActiveProfiles, globalSettings.ActiveProfiles...))
}
return s
@@ -89,4 +113,16 @@ func expandAllEnvPlaceholders(s *settings) {
s.Servers[i].Username = evaluateVariable(server.Username, nil, nil)
s.Servers[i].Password = evaluateVariable(server.Password, nil, nil)
}
for i, profile := range s.Profiles {
s.Profiles[i].ID = evaluateVariable(profile.ID, nil, nil)
for j, repo := range profile.Repositories {
s.Profiles[i].Repositories[j].ID = evaluateVariable(repo.ID, nil, nil)
s.Profiles[i].Repositories[j].Name = evaluateVariable(repo.Name, nil, nil)
s.Profiles[i].Repositories[j].URL = evaluateVariable(repo.URL, nil, nil)
}
}
for i, activeProfile := range s.ActiveProfiles {
s.ActiveProfiles[i] = evaluateVariable(activeProfile, nil, nil)
}
}

View File

@@ -1,6 +1,7 @@
package pom
import (
"net/url"
"path/filepath"
"testing"
@@ -35,6 +36,39 @@ func Test_ReadSettings(t *testing.T) {
Username: "test-user-only",
},
},
Profiles: []Profile{
{
ID: "mycompany-global",
Repositories: []pomRepository{
{
ID: "mycompany-internal-releases",
URL: "https://mycompany.example.com/repository/internal-releases",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
{
ID: "mycompany-global-releases",
URL: "https://mycompany.example.com/repository/global-releases",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
},
ActiveByDefault: false,
},
{
ID: "default",
Repositories: []pomRepository{
{
ID: "mycompany-default-releases",
URL: "https://mycompany.example.com/repository/default-releases",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
},
ActiveByDefault: true,
},
},
ActiveProfiles: []string{},
},
},
{
@@ -59,18 +93,41 @@ func Test_ReadSettings(t *testing.T) {
Username: "test-user-only",
},
},
Profiles: []Profile{
{
ID: "mycompany-global",
Repositories: []pomRepository{
{
ID: "mycompany-releases",
URL: "https://mycompany.example.com/repository/user-releases",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
{
ID: "mycompany-user-snapshots",
URL: "https://mycompany.example.com/repository/user-snapshots",
ReleasesEnabled: "false",
SnapshotsEnabled: "true",
},
},
ActiveByDefault: true,
},
},
ActiveProfiles: []string{
"mycompany-global",
},
},
},
{
// $ mvn help:effective-settings
//[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
//[INFO] --- maven-help-plugin:3.4.0:effective-settings (default-cli) @ standalone-pom ---
//Effective user-specific configuration settings:
// [INFO] ------------------< org.apache.maven:standalone-pom >-------------------
// [INFO] --- maven-help-plugin:3.4.0:effective-settings (default-cli) @ standalone-pom ---
// Effective user-specific configuration settings:
//
//<?xml version="1.0" encoding="UTF-8"?>
//<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
// <?xml version="1.0" encoding="UTF-8"?>
// <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
// <localRepository>/root/testdata/user/repository</localRepository>
// <servers>
// <servers>
// <server>
// <id>user-server</id>
// </server>
@@ -87,7 +144,53 @@ func Test_ReadSettings(t *testing.T) {
// <id>global-server</id>
// </server>
// </servers>
//</settings>
// <profiles>
// <profile>
// <activation>
// <activeByDefault>true</activeByDefault>
// </activation>
// <repositories>
// <repository>
// <releases>
// <checksumPolicy>fail</checksumPolicy>
// </releases>
// <snapshots>
// <enabled>false</enabled>
// </snapshots>
// <id>mycompany-releases</id>
// <url>https://mycompany.example.com/repository/user-releases</url>
// </repository>
// <repository>
// <releases>
// <enabled>false</enabled>
// </releases>
// <snapshots />
// <id>mycompany-user-snapshots</id>
// <url>https://mycompany.example.com/repository/user-snapshots</url>
// </repository>
// </repositories>
// <id>mycompany-global</id>
// </profile>
// <profile>
// <activation>
// <activeByDefault>true</activeByDefault>
// </activation>
// <repositories>
// <repository>
// <releases />
// <snapshots>
// <enabled>false</enabled>
// </snapshots>
// <id>mycompany-default-releases</id>
// <url>https://mycompany.example.com/repository/default-releases</url>
// </repository>
// </repositories>
// </profile>
// </profiles>
// <activeProfiles>
// <activeProfile>mycompany-global</activeProfile>
// </activeProfiles>
// </settings>
name: "happy path with global and user settings",
envs: map[string]string{
"HOME": filepath.Join("testdata", "settings", "user"),
@@ -112,6 +215,41 @@ func Test_ReadSettings(t *testing.T) {
ID: "global-server",
},
},
Profiles: []Profile{
{
ID: "mycompany-global",
Repositories: []pomRepository{
{
ID: "mycompany-releases",
URL: "https://mycompany.example.com/repository/user-releases",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
{
ID: "mycompany-user-snapshots",
URL: "https://mycompany.example.com/repository/user-snapshots",
ReleasesEnabled: "false",
SnapshotsEnabled: "true",
},
},
ActiveByDefault: true,
},
{
ID: "default",
Repositories: []pomRepository{
{
ID: "mycompany-default-releases",
URL: "https://mycompany.example.com/repository/default-releases",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
},
ActiveByDefault: true,
},
},
ActiveProfiles: []string{
"mycompany-global",
},
},
},
{
@@ -132,6 +270,9 @@ func Test_ReadSettings(t *testing.T) {
"SERVER_ID": "server-id-from-env",
"USERNAME": "username-from-env",
"PASSWORD": "password-from-env",
"PROFILE_ID": "mycompany-global",
"REPO_ID": "mycompany-releases",
"REPO_URL": "https://mycompany.example.com",
},
wantSettings: settings{
LocalRepository: "part1/part2/.m2/repository",
@@ -149,6 +290,22 @@ func Test_ReadSettings(t *testing.T) {
Username: "test-user-only",
},
},
Profiles: []Profile{
{
ID: "mycompany-global",
Repositories: []pomRepository{
{
ID: "mycompany-releases",
URL: "https://mycompany.example.com/repository/user-releases",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
},
},
},
ActiveProfiles: []string{
"mycompany-global",
},
},
},
}
@@ -163,3 +320,156 @@ func Test_ReadSettings(t *testing.T) {
})
}
}
func Test_effectiveRepositories(t *testing.T) {
tests := []struct {
name string
s settings
want []repository
}{
{
name: "single active profile, reversed order",
s: settings{
Servers: []Server{
{
ID: "r1",
Username: "u",
Password: "p",
},
},
Profiles: []Profile{
{
ID: "p1",
Repositories: []pomRepository{
{
ID: "r1",
URL: "https://example.com/repo1",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
{
ID: "r2",
URL: "https://example.com/repo2",
ReleasesEnabled: "false",
SnapshotsEnabled: "true",
},
},
},
},
ActiveProfiles: []string{"p1"},
},
want: []repository{
{
url: mustParseURL(t, "https://example.com/repo2"),
releaseEnabled: false,
snapshotEnabled: true,
},
{
url: mustParseURL(t, "https://u:p@example.com/repo1"),
releaseEnabled: true,
snapshotEnabled: false,
},
},
},
{
name: "activeByDefault + activeProfiles with dedup and reverse",
s: settings{
Servers: nil,
Profiles: []Profile{
{
ID: "p1",
ActiveByDefault: true,
Repositories: []pomRepository{
{
ID: "dup",
URL: "https://p1.example.com/dup",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
{
ID: "only-p1",
URL: "https://p1.example.com/only",
ReleasesEnabled: "true",
SnapshotsEnabled: "true",
},
},
},
{
ID: "p2",
Repositories: []pomRepository{
{
ID: "dup",
URL: "https://p2.example.com/dup",
ReleasesEnabled: "true",
SnapshotsEnabled: "true",
},
},
},
},
ActiveProfiles: []string{"p2"},
},
// Expected order after dedup (keep first occurrence from p1) and reverse:
// Input order before reverse: [dup(from p1), only-p1, dup(from p2 - removed by dedup)]
// After reverse: [only-p1, dup(from p1)]
want: []repository{
{
url: mustParseURL(t, "https://p1.example.com/only"),
releaseEnabled: true,
snapshotEnabled: true,
},
{
url: mustParseURL(t, "https://p1.example.com/dup"),
releaseEnabled: true,
snapshotEnabled: false,
},
},
},
{
name: "disabled repositories are ignored",
s: settings{
Profiles: []Profile{
{
ID: "p",
ActiveByDefault: true,
Repositories: []pomRepository{
{
ID: "disabled",
URL: "https://example.com/disabled",
ReleasesEnabled: "false",
SnapshotsEnabled: "false",
},
{
ID: "enabled",
URL: "https://example.com/enabled",
ReleasesEnabled: "true",
SnapshotsEnabled: "false",
},
},
},
},
},
want: []repository{
{
url: mustParseURL(t, "https://example.com/enabled"),
releaseEnabled: true,
snapshotEnabled: false,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.s.effectiveRepositories()
require.Equal(t, tt.want, got)
})
}
}
// mustParseURL parses a URL and panics on error; handy for test literals
func mustParseURL(t *testing.T, s string) url.URL {
t.Helper()
u, err := url.Parse(s)
require.NoError(t, err)
return *u
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>example-api</artifactId>
<version>1.7.30</version>
<packaging>jar</packaging>
<name>Example API Module</name>
<description>The example API</description>
<licenses>
<license>
<name>Custom License from custom repo</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
</project>

View File

@@ -18,4 +18,49 @@
<username>test-user-only</username>
</server>
</servers>
<profiles>
<profile>
<id>mycompany-global</id>
<repositories>
<repository>
<id>mycompany-internal-releases</id>
<url>https://mycompany.example.com/repository/internal-releases</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>mycompany-global-releases</id>
<url>https://mycompany.example.com/repository/global-releases</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</profile>
<profile>
<id>default</id>
<repositories>
<repository>
<id>mycompany-default-releases</id>
<url>https://mycompany.example.com/repository/default-releases</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
</settings>

View File

@@ -18,4 +18,25 @@
<username>test-user-only</username>
</server>
</servers>
<profiles>
<profile>
<id>${env.PROFILE_ID}</id>
<repositories>
<repository>
<id>${env.REPO_ID}</id>
<url>${env.REPO_URL}/repository/user-releases</url>
<releases>
<enabled>true</enabled>
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>${env.PROFILE_ID}</activeProfile>
</activeProfiles>
</settings>

View File

@@ -18,4 +18,38 @@
<username>test-user-only</username>
</server>
</servers>
<profiles>
<profile>
<id>mycompany-global</id>
<repositories>
<repository>
<id>mycompany-releases</id>
<url>https://mycompany.example.com/repository/user-releases</url>
<releases>
<enabled>true</enabled>
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>mycompany-user-snapshots</id>
<url>https://mycompany.example.com/repository/user-snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
<activeProfiles>
<activeProfile>mycompany-global</activeProfile>
</activeProfiles>
</settings>