fix org repo creation being limited by user limits ()

fixes an issue where user is unable to create new repository in
organization via UI if repository limits are in place and user has
exhausted them for their own namespace.

closes: https://github.com/go-gitea/gitea/issues/15504

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
pull/34001/head^2
TheFox0x7 2025-03-27 14:16:17 +01:00 committed by GitHub
parent a7594969b6
commit 053592d847
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 37 additions and 27 deletions
routers/web/repo
templates/repo
tests/integration
web_src/js/features

View File

@ -154,8 +154,8 @@ func createCommon(ctx *context.Context) {
ctx.Data["Licenses"] = repo_module.Licenses ctx.Data["Licenses"] = repo_module.Licenses
ctx.Data["Readmes"] = repo_module.Readmes ctx.Data["Readmes"] = repo_module.Readmes
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo() ctx.Data["CanCreateRepoInDoer"] = ctx.Doer.CanCreateRepo()
ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit() ctx.Data["MaxCreationLimitOfDoer"] = ctx.Doer.MaxCreationLimit()
ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
} }

View File

@ -7,25 +7,21 @@
<div class="ui attached segment"> <div class="ui attached segment">
{{template "base/alert" .}} {{template "base/alert" .}}
{{template "repo/create_helper" .}} {{template "repo/create_helper" .}}
{{if not .CanCreateRepo}}
<div class="ui negative message">
<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
</div>
{{end}}
<form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post"> <form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
<div class="inline required field {{if .Err_Owner}}error{{end}}"> <div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label> <label>{{ctx.Locale.Tr "repo.owner"}}</label>
<div class="ui selection owner dropdown"> <div class="ui selection dropdown" id="repo_owner_dropdown">
<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> <input type="hidden" name="uid" value="{{.ContextUser.ID}}">
<span class="text truncated-item-container" title="{{.ContextUser.Name}}"> <span class="text truncated-item-name"></span>
{{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}}
<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
{{if not .CanCreateRepoInDoer}}
data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimitOfDoer}}"
{{end}}
>
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div> </div>
@ -212,7 +208,7 @@
<br> <br>
<div class="inline field"> <div class="inline field">
<label></label> <label></label>
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}"> <button class="ui primary button">
{{ctx.Locale.Tr "repo.create_repo"}} {{ctx.Locale.Tr "repo.create_repo"}}
</button> </button>
</div> </div>

View File

@ -31,16 +31,16 @@ func testRepoGenerate(t *testing.T, session *TestSession, templateID, templateOw
// Step2: click the "Use this template" button // Step2: click the "Use this template" button
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/create\"]").Attr("href") link, exists := htmlDoc.doc.Find(`a.ui.button[href^="/repo/create"]`).Attr("href")
assert.True(t, exists, "The template has changed") assert.True(t, exists, "The template has changed")
req = NewRequest(t, "GET", link) req = NewRequest(t, "GET", link)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
// Step3: fill the form of the create // Step3: fill the form on the "create" page
htmlDoc = NewHTMLParser(t, resp.Body) htmlDoc = NewHTMLParser(t, resp.Body)
link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/create\"]").Attr("action") link, exists = htmlDoc.doc.Find(`form.ui.form[action^="/repo/create"]`).Attr("action")
assert.True(t, exists, "The template has changed") assert.True(t, exists, "The template has changed")
_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value") _, exists = htmlDoc.doc.Find(fmt.Sprintf(`#repo_owner_dropdown .item[data-value="%d"]`, generateOwner.ID)).Attr("data-value")
assert.True(t, exists, "Generate owner '%s' is not present in select box", generateOwnerName) assert.True(t, exists, "Generate owner '%s' is not present in select box", generateOwnerName)
req = NewRequestWithValues(t, "POST", link, map[string]string{ req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": htmlDoc.GetCSRF(), "_csrf": htmlDoc.GetCSRF(),

View File

@ -1,4 +1,4 @@
import {hideElem, showElem, toggleElem} from '../utils/dom.ts'; import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts';
import {htmlEscape} from 'escape-goat'; import {htmlEscape} from 'escape-goat';
import {fomanticQuery} from '../modules/fomantic/base.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts';
import {sanitizeRepoName} from './repo-common.ts'; import {sanitizeRepoName} from './repo-common.ts';
@ -6,7 +6,9 @@ import {sanitizeRepoName} from './repo-common.ts';
const {appSubUrl} = window.config; const {appSubUrl} = window.config;
function initRepoNewTemplateSearch(form: HTMLFormElement) { function initRepoNewTemplateSearch(form: HTMLFormElement) {
const inputRepoOwnerUid = form.querySelector<HTMLInputElement>('#uid'); const elSubmitButton = querySingleVisibleElem<HTMLInputElement>(form, '.ui.primary.button');
const elCreateRepoErrorMessage = form.querySelector('#create-repo-error-message');
const elRepoOwnerDropdown = form.querySelector('#repo_owner_dropdown');
const elRepoTemplateDropdown = form.querySelector<HTMLInputElement>('#repo_template_search'); const elRepoTemplateDropdown = form.querySelector<HTMLInputElement>('#repo_template_search');
const inputRepoTemplate = form.querySelector<HTMLInputElement>('#repo_template'); const inputRepoTemplate = form.querySelector<HTMLInputElement>('#repo_template');
const elTemplateUnits = form.querySelector('#template_units'); const elTemplateUnits = form.querySelector('#template_units');
@ -19,11 +21,23 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
inputRepoTemplate.addEventListener('change', checkTemplate); inputRepoTemplate.addEventListener('change', checkTemplate);
checkTemplate(); checkTemplate();
const $dropdown = fomanticQuery(elRepoTemplateDropdown); const $repoOwnerDropdown = fomanticQuery(elRepoOwnerDropdown);
const $repoTemplateDropdown = fomanticQuery(elRepoTemplateDropdown);
const onChangeOwner = function () { const onChangeOwner = function () {
$dropdown.dropdown('setting', { const ownerId = $repoOwnerDropdown.dropdown('get value');
const $ownerItem = $repoOwnerDropdown.dropdown('get item', ownerId);
hideElem(elCreateRepoErrorMessage);
elSubmitButton.disabled = false;
if ($ownerItem?.length) {
const elOwnerItem = $ownerItem[0];
elCreateRepoErrorMessage.textContent = elOwnerItem.getAttribute('data-create-repo-disallowed-prompt') ?? '';
const hasError = Boolean(elCreateRepoErrorMessage.textContent);
toggleElem(elCreateRepoErrorMessage, hasError);
elSubmitButton.disabled = hasError;
}
$repoTemplateDropdown.dropdown('setting', {
apiSettings: { apiSettings: {
url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`, url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${ownerId}`,
onResponse(response: any) { onResponse(response: any) {
const results = []; const results = [];
results.push({name: '', value: ''}); // empty item means not using template results.push({name: '', value: ''}); // empty item means not using template
@ -33,14 +47,14 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
value: String(tmplRepo.repository.id), value: String(tmplRepo.repository.id),
}); });
} }
$dropdown.fomanticExt.onResponseKeepSelectedItem($dropdown, inputRepoTemplate.value); $repoTemplateDropdown.fomanticExt.onResponseKeepSelectedItem($repoTemplateDropdown, inputRepoTemplate.value);
return {results}; return {results};
}, },
cache: false, cache: false,
}, },
}); });
}; };
inputRepoOwnerUid.addEventListener('change', onChangeOwner); $repoOwnerDropdown.dropdown('setting', 'onChange', onChangeOwner);
onChangeOwner(); onChangeOwner();
} }