diff --git a/src/main/java/org/jenkinsci/plugin/gitea/GiteaOwnerListHelper.java b/src/main/java/org/jenkinsci/plugin/gitea/GiteaOwnerListHelper.java new file mode 100644 index 00000000..e491d25a --- /dev/null +++ b/src/main/java/org/jenkinsci/plugin/gitea/GiteaOwnerListHelper.java @@ -0,0 +1,58 @@ +package org.jenkinsci.plugin.gitea; + +import hudson.util.ListBoxModel; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.jenkinsci.plugin.gitea.client.api.GiteaRepository; + +/** + * Utility class for returning unique Gitea repository owners. + */ +public class GiteaOwnerListHelper { + /** + * Retrieves unique repository owners from the given list of repositories. + * + * @param currentUser the Gitea user used for fetching the repositories + * @param repositories the list of repositories to extract owners from + * @return a Set of unique owner names + */ + public static Set getOwners(String currentUser, + List repositories) { + Set owners = new HashSet<>(); + + for (GiteaRepository repo : repositories) { + String owner = repo.getOwner().getUsername(); + owners.add(owner); // Set will handle duplicates + } + + owners.add(currentUser); // Ensure current user is included + + return owners; + } + + /** + * Returns a ListBoxModel with repository owners. + * + * @param currentUser the Gitea user used for fetching the repositories + * @param currentOwner the currently selected owner (will be preserved if valid) + * @param repositories the list of repositories to extract owners from + * @return the populated ListBoxModel + */ + public static ListBoxModel populateOwnerListBoxModel(String currentUser, + String currentOwner, + List repositories) { + ListBoxModel result = new ListBoxModel(); + Set owners = getOwners(currentUser, repositories); + // Add owners to the result, with current selection first if it exists + if (owners.remove(currentOwner)) { + result.add(currentOwner); + } + + for (String owner : owners) { + result.add(owner); + } + + return result; + } +} diff --git a/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMNavigator.java b/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMNavigator.java index a2b3bc42..6c15776b 100644 --- a/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMNavigator.java +++ b/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMNavigator.java @@ -51,6 +51,8 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import jenkins.authentication.tokens.api.AuthenticationTokens; import jenkins.model.Jenkins; import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; @@ -86,10 +88,12 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; public class GiteaSCMNavigator extends SCMNavigator { private final String serverUrl; private final String repoOwner; + private static final Logger LOGGER = Logger.getLogger(GiteaSCMNavigator.class.getName()); private String credentialsId; private List>> traits = new ArrayList<>(); private GiteaOwner giteaOwner; @@ -385,6 +389,62 @@ public FormValidation doCheckCredentialsId(@AncestorInPath SCMSourceOwner contex return FormValidation.ok(); } + @POST + public ListBoxModel doFillRepoOwnerItems(@AncestorInPath SCMSourceOwner context, + @QueryParameter String serverUrl, + @QueryParameter String credentialsId, + @QueryParameter String repoOwner) throws IOException, + InterruptedException { + ListBoxModel result = new ListBoxModel(); + if (context == null) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + result.add(repoOwner); + return result; + } + } else { + if (!context.hasPermission(Item.EXTENDED_READ) + && !context.hasPermission(CredentialsProvider.USE_ITEM)) { + result.add(repoOwner); + return result; + } + } + GiteaServer server = GiteaServers.get().findServer(serverUrl); + if (server == null) { + result.add(repoOwner); + return result; + } + StandardCredentials credentials = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StandardCredentials.class, + context, + context instanceof Queue.Task + ? ((Queue.Task) context).getDefaultAuthentication() + : ACL.SYSTEM, + URIRequirementBuilder.fromUri(serverUrl).build() + ), + CredentialsMatchers.allOf( + AuthenticationTokens.matcher(GiteaAuth.class), + CredentialsMatchers.withId(credentialsId) + ) + ); + if (credentials == null) { + result.add(repoOwner); + return result; + } + + try (GiteaConnection c = Gitea.server(serverUrl) + .as(AuthenticationTokens.convert(GiteaAuth.class, credentials)) + .open()) { + List repositories = c.fetchCurrentUserRepositories(); + String currentUser = c.fetchCurrentUser().getUsername(); + return GiteaOwnerListHelper.populateOwnerListBoxModel(currentUser, repoOwner, repositories); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Could not populate owners", e); + } + + return result; + } + @NonNull @Override public String getDescription() { diff --git a/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMSource.java b/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMSource.java index 95c33705..56cb9b33 100644 --- a/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMSource.java +++ b/src/main/java/org/jenkinsci/plugin/gitea/GiteaSCMSource.java @@ -112,6 +112,7 @@ import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.verb.POST; public class GiteaSCMSource extends AbstractGitSCMSource { public static final VersionNumber TAG_SUPPORT_MINIMUM_VERSION = new VersionNumber("1.9.0"); @@ -919,6 +920,63 @@ public FormValidation doCheckCredentialsId(@AncestorInPath SCMSourceOwner contex return FormValidation.ok(); } + @POST + public ListBoxModel doFillRepoOwnerItems(@AncestorInPath SCMSourceOwner context, + @QueryParameter String serverUrl, + @QueryParameter String credentialsId, + @QueryParameter String repoOwner) throws IOException, + InterruptedException { + ListBoxModel result = new ListBoxModel(); + if (context == null) { + if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { + // must have admin if you want the list without a context + result.add(repoOwner); + return result; + } + } else { + if (!context.hasPermission(Item.EXTENDED_READ) + && !context.hasPermission(CredentialsProvider.USE_ITEM)) { + result.add(repoOwner); + return result; + } + } + GiteaServer server = GiteaServers.get().findServer(serverUrl); + if (server == null) { + result.add(repoOwner); + return result; + } + StandardCredentials credentials = CredentialsMatchers.firstOrNull( + CredentialsProvider.lookupCredentials( + StandardCredentials.class, + context, + context instanceof Queue.Task + ? ((Queue.Task) context).getDefaultAuthentication() + : ACL.SYSTEM, + URIRequirementBuilder.fromUri(serverUrl).build() + ), + CredentialsMatchers.allOf( + AuthenticationTokens.matcher(GiteaAuth.class), + CredentialsMatchers.withId(credentialsId) + ) + ); + if (credentials == null) { + result.add(repoOwner); + return result; + } + + try (GiteaConnection c = Gitea.server(serverUrl) + .as(AuthenticationTokens.convert(GiteaAuth.class, credentials)) + .open()) { + List repositories = c.fetchCurrentUserRepositories(); + String currentUser = c.fetchCurrentUser().getUsername(); + return GiteaOwnerListHelper.populateOwnerListBoxModel(currentUser, repoOwner, repositories); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Could not populate owners", e); + } + + return result; + } + public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String serverUrl, @QueryParameter String credentialsId, diff --git a/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMNavigator/config.jelly b/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMNavigator/config.jelly index 3d36263b..bc0fdf8f 100644 --- a/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMNavigator/config.jelly +++ b/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMNavigator/config.jelly @@ -8,7 +8,7 @@ - + diff --git a/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMSource/config-detail.jelly b/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMSource/config-detail.jelly index ef462bdf..de75539c 100644 --- a/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMSource/config-detail.jelly +++ b/src/main/resources/org/jenkinsci/plugin/gitea/GiteaSCMSource/config-detail.jelly @@ -8,10 +8,10 @@ - + - + diff --git a/src/test/java/org/jenkinsci/plugin/gitea/GiteaOwnerListHelperTest.java b/src/test/java/org/jenkinsci/plugin/gitea/GiteaOwnerListHelperTest.java new file mode 100644 index 00000000..f0a54066 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugin/gitea/GiteaOwnerListHelperTest.java @@ -0,0 +1,112 @@ +package org.jenkinsci.plugin.gitea; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.jenkinsci.plugin.gitea.client.api.GiteaOwner; +import org.jenkinsci.plugin.gitea.client.api.GiteaRepository; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class GiteaOwnerListHelperTest { + + @Test + void getOwners_withValidRepositories_returnsUniqueOwnersIncludingCurrentUser() { + List repositories = Arrays.asList( + createMockRepository("alice"), + createMockRepository("bob"), + createMockRepository("alice") // duplicate + ); + + Set owners = GiteaOwnerListHelper.getOwners("charlie", repositories); + + assertThat(owners, containsInAnyOrder("alice", "bob", "charlie")); + } + + @Test + void getOwners_withEmptyRepositories_returnsOnlyCurrentUser() { + List repositories = Collections.emptyList(); + + Set owners = GiteaOwnerListHelper.getOwners("charlie", repositories); + + assertThat(owners.contains("charlie"), is(true)); + } + + @Test + void populateOwnerListBoxModel_withCurrentOwnerSelected_placesCurrentOwnerFirst() { + List repositories = Arrays.asList( + createMockRepository("alice"), + createMockRepository("bob")); + + var result = GiteaOwnerListHelper.populateOwnerListBoxModel("charlie", "bob", repositories); + + assertThat(result.size(), is(3)); + assertThat(result.get(0).name, is("bob")); + assertThat(result.get(1).name, anyOf(is("alice"), is("charlie"))); + assertThat(result.get(2).name, anyOf(is("alice"), is("charlie"))); + } + + @Test + void populateOwnerListBoxModel_withCurrentOwnerNotInList_includesCurrentOwner() { + List repositories = Arrays.asList( + createMockRepository("alice")); + + var result = GiteaOwnerListHelper.populateOwnerListBoxModel("bob", "charlie", repositories); + + assertThat(result.size(), is(2)); + assertThat(result.stream().anyMatch(opt -> "charlie".equals(opt.name)), is(false)); + assertThat(result.stream().anyMatch(opt -> "alice".equals(opt.name)), is(true)); + assertThat(result.stream().anyMatch(opt -> "bob".equals(opt.name)), is(true)); + } + + @Test + void populateOwnerListBoxModel_selectedOwnerIsFirst_restAreUnordered() { + List repositories = Arrays.asList( + createMockRepository("alice"), + createMockRepository("bob"), + createMockRepository("charlie") + ); + + var result = GiteaOwnerListHelper.populateOwnerListBoxModel("dave", "bob", repositories); + + assertThat(result.size(), is(4)); + assertThat(result.get(0).name, is("bob")); // selected owner first + + // The rest should be alice, charlie, dave in any order + List rest = Arrays.asList(result.get(1).name, result.get(2).name, result.get(3).name); + assertThat(rest, containsInAnyOrder("alice", "charlie", "dave")); + } + + @Test + void populateOwnerListBoxModel_selectedOwnerNotPresent() { + List repositories = Arrays.asList( + createMockRepository("alice"), + createMockRepository("charlie") + ); + + var result = GiteaOwnerListHelper.populateOwnerListBoxModel("dave", "bob", repositories); + + assertThat(result.size(), is(3)); + assertThat(result.get(0).name, not(is("bob"))); // selected owner first + + // The rest should be alice, charlie, dave in any order + List rest = Arrays.asList(result.get(0).name, result.get(1).name, result.get(2).name); + assertThat(rest, containsInAnyOrder("alice", "charlie", "dave")); + } + + private GiteaRepository createMockRepository(String ownerName) { + GiteaRepository repo = mock(GiteaRepository.class); + GiteaOwner owner = mock(GiteaOwner.class); + when(owner.getUsername()).thenReturn(ownerName); + when(repo.getOwner()).thenReturn(owner); + return repo; + } +}