From dea71bb8ba8444bdbc1fa1f466ec6b6787a52097 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 18 Aug 2026 01:43:34 +0800 Subject: [PATCH] enhance: user-friendly packages setup manual (#38946) * replace #35564 * fix #36992 --- modules/htmlutil/html.go | 2 +- modules/htmlutil/html_test.go | 2 +- modules/templates/util_render.go | 12 +-- options/locale/locale_en-US.json | 1 - routers/web/user/package.go | 27 +----- services/packages/debian/spec.go | 87 +++++++++++++++++++ services/packages/packages.go | 2 + services/packages/pkgspec/manager.go | 2 + services/packages/spec.go | 34 ++++++-- services/packages/terraform/spec.go | 4 +- templates/package/content/debian.tmpl | 12 ++- tests/integration/api_packages_debian_test.go | 4 + web_src/css/repo/packages.css | 18 ++++ web_src/js/features/packages.test.ts | 18 ++++ web_src/js/features/packages.ts | 24 +++++ web_src/js/index.ts | 2 + 16 files changed, 205 insertions(+), 46 deletions(-) create mode 100644 services/packages/debian/spec.go create mode 100644 web_src/js/features/packages.test.ts create mode 100644 web_src/js/features/packages.ts diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index a480826bc61..cba73e1fb83 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -149,7 +149,7 @@ func (b *HTMLBuilder) WriteHTML(s template.HTML) *HTMLBuilder { return b } -func (b *HTMLBuilder) WriteFormat(fmt template.HTML, args ...any) *HTMLBuilder { +func (b *HTMLBuilder) WriteFormatf(fmt template.HTML, args ...any) *HTMLBuilder { _, _ = HTMLPrintf(&b.sb, fmt, args...) return b } diff --git a/modules/htmlutil/html_test.go b/modules/htmlutil/html_test.go index 88e4935a1df..a499bfba8a1 100644 --- a/modules/htmlutil/html_test.go +++ b/modules/htmlutil/html_test.go @@ -26,7 +26,7 @@ func TestHTMLFormat(t *testing.T) { func TestHTMLBuilder(t *testing.T) { b := &HTMLBuilder{} - b.WriteString("<").WriteHTML("
").WriteFormat("%s%s", ">", EscapeString(">")) + b.WriteString("<").WriteHTML("
").WriteFormatf("%s%s", ">", EscapeString(">")) assert.Equal(t, "<
>>", b.String()) assert.Equal(t, template.HTML("<
>>"), b.HTMLString()) } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index e47dfb4e819..0197ab60451 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -227,7 +227,7 @@ func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink strin if label == nil { continue } - htmlCode.WriteFormat(``, baseLink, label.ID) + htmlCode.WriteFormatf(``, baseLink, label.ID) htmlCode.WriteHTML(ut.RenderLabel(label)) htmlCode.WriteHTML("") } @@ -347,7 +347,7 @@ func (ut *RenderUtils) AvatarStack(data *user_model.AvatarStackData) template.HT var b htmlutil.HTMLBuilder b.WriteHTML(``) if overflow > 0 { - b.WriteFormat(`+%d`, overflow, overflow) + b.WriteFormatf(`+%d`, overflow, overflow) } // FIXME: such "backward" breaks a11y like screen readers @@ -361,9 +361,9 @@ func (ut *RenderUtils) AvatarStack(data *user_model.AvatarStackData) template.HT func (ut *RenderUtils) writeAvatarStackItem(b *htmlutil.HTMLBuilder, data *user_model.AvatarStackData, participant *user_model.CommitParticipant) { avatar := ut.participantAvatar(participant) if href := ut.participantHref(data, participant); href != "" { - b.WriteFormat(`%s`, href, avatar) + b.WriteFormatf(`%s`, href, avatar) } else { - b.WriteFormat(`%s`, avatar) + b.WriteFormatf(`%s`, avatar) } } @@ -392,10 +392,10 @@ func (ut *RenderUtils) AvatarStackWithNames(data *user_model.AvatarStackData) te b.WriteHTML(ut.participantNameLink(data, participants[0])) case 2: b.WriteHTML(ut.participantNameLink(data, participants[0])) - b.WriteFormat(`%s`, locale.Tr("repo.commits.avatar_stack_and")) + b.WriteFormatf(`%s`, locale.Tr("repo.commits.avatar_stack_and")) b.WriteHTML(ut.participantNameLink(data, participants[1])) default: - b.WriteFormat(``, + b.WriteFormatf(``, locale.Tr("repo.commits.avatar_stack_people", len(participants))) b.WriteHTML(`
`) for _, participant := range participants { diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 47e946a37fe..fb76a2dacde 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3600,7 +3600,6 @@ "packages.cran.registry": "Set up this registry in your Rprofile.site file:", "packages.cran.install": "To install the package, run the following command:", "packages.debian.registry": "Set up this registry from the command line:", - "packages.debian.registry.info": "Choose $distribution and $component from the list below.", "packages.debian.install": "To install the package, run the following command:", "packages.debian.repository": "Repository Info", "packages.debian.repository.distributions": "Distributions", diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 102e347d9c8..bd363bfc8ce 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -23,7 +23,6 @@ import ( alpine_module "gitea.dev/modules/packages/alpine" arch_module "gitea.dev/modules/packages/arch" container_module "gitea.dev/modules/packages/container" - debian_module "gitea.dev/modules/packages/debian" rpm_module "gitea.dev/modules/packages/rpm" terraform_module "gitea.dev/modules/packages/terraform" "gitea.dev/modules/setting" @@ -245,27 +244,6 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["Repositories"] = util.Sorted(repositories.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values()) - case packages_model.TypeDebian: - distributions := make(container.Set[string]) - components := make(container.Set[string]) - architectures := make(container.Set[string]) - - for _, f := range pd.Files { - for _, pp := range f.Properties { - switch pp.Name { - case debian_module.PropertyDistribution: - distributions.Add(pp.Value) - case debian_module.PropertyComponent: - components.Add(pp.Value) - case debian_module.PropertyArchitecture: - architectures.Add(pp.Value) - } - } - } - - ctx.Data["Distributions"] = util.Sorted(distributions.Values()) - ctx.Data["Components"] = util.Sorted(components.Values()) - ctx.Data["Architectures"] = util.Sorted(architectures.Values()) case packages_model.TypeRpm: groups := make(container.Set[string]) architectures := make(container.Set[string]) @@ -318,12 +296,15 @@ func ViewPackageVersion(ctx *context.Context) { } ctx.Data["LatestVersions"] = pvs ctx.Data["TotalVersionCount"] = pvsTotal - ctx.Data["PackageVersionViewData"], err = packages_service.GetSpecManager().Get(pd.Package.Type).GetViewPackageVersionData(ctx, pd) + pkgSpec := packages_service.GetSpecManager().Get(pd.Package.Type) + viewData, err := pkgSpec.GetViewPackageVersionData(ctx, pd) if err != nil { ctx.ServerError("GetViewPackageVersionData", err) return } + ctx.Data["PackageVersionViewData"] = viewData + ctx.Data["PackageVersionSetupManual"] = pkgSpec.RenderSetupManual(ctx, pd, viewData) ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() hasRepositoryAccess := false diff --git a/services/packages/debian/spec.go b/services/packages/debian/spec.go new file mode 100644 index 00000000000..cab6d49e80b --- /dev/null +++ b/services/packages/debian/spec.go @@ -0,0 +1,87 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package debian + +import ( + "context" + "html/template" + "strings" + + packages_model "gitea.dev/models/packages" + "gitea.dev/modules/container" + "gitea.dev/modules/htmlutil" + "gitea.dev/modules/httplib" + debian_module "gitea.dev/modules/packages/debian" + "gitea.dev/modules/util" + packages_service "gitea.dev/services/packages" +) + +type Specialization struct { + packages_service.SpecOnBeforeRemovePackageAll + packages_service.SpecOnBeforeRemovePackageVersion +} + +var _ packages_service.Specialization = (*Specialization)(nil) + +type debianViewData struct { + Distributions []string + Components []string + Architectures []string +} + +func (s Specialization) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) { + distributions := make(container.Set[string]) + components := make(container.Set[string]) + architectures := make(container.Set[string]) + for _, f := range pd.Files { + for _, pp := range f.Properties { + switch pp.Name { + case debian_module.PropertyDistribution: + distributions.Add(pp.Value) + case debian_module.PropertyComponent: + components.Add(pp.Value) + case debian_module.PropertyArchitecture: + architectures.Add(pp.Value) + } + } + } + viewData := debianViewData{ + Distributions: util.Sorted(distributions.Values()), + Components: util.Sorted(components.Values()), + Architectures: util.Sorted(architectures.Values()), + } + return viewData, nil +} + +func writeVarSelect(w *htmlutil.HTMLBuilder, varName string, varItems []string) { + w.WriteFormatf(`
%s=
") +} + +func (s Specialization) RenderSetupManual(ctx context.Context, pkg *packages_model.PackageDescriptor, viewDataAny any) template.HTML { + viewData := viewDataAny.(debianViewData) //nolint:forcetypeassert // must be valid + const nl = "\n" + appFullLink := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + + w := &htmlutil.HTMLBuilder{} + w.WriteHTML(`
`) + { + w.WriteHTML(`
`) + writeVarSelect(w, "distribution", viewData.Distributions) + writeVarSelect(w, "component", viewData.Components) + w.WriteHTML(`
`) + } + { + w.WriteHTML(`
`)
+		w.WriteFormatf(`sudo curl %s/api/packages/%s/debian/repository.key -o /etc/apt/keyrings/gitea-%s.asc`+nl+nl, appFullLink, pkg.Owner.Name, pkg.Owner.Name)
+		w.WriteFormatf(`echo "deb [signed-by=/etc/apt/keyrings/gitea-%s.asc] %s/api/packages/%s/debian $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list`+nl+nl, pkg.Owner.Name, appFullLink, pkg.Owner.Name)
+		w.WriteHTML(`sudo apt update` + nl)
+		w.WriteHTML(`
`) + } + w.WriteHTML(`
`) + return w.HTMLString() +} diff --git a/services/packages/packages.go b/services/packages/packages.go index abd4b4c54db..68e43cfab5d 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "errors" "fmt" + "html/template" "io" "net/http" "net/url" @@ -37,6 +38,7 @@ type Specialization interface { OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) + RenderSetupManual(ctx context.Context, pkg *packages_model.PackageDescriptor, viewData any) template.HTML } // PackageInfo describes a package diff --git a/services/packages/pkgspec/manager.go b/services/packages/pkgspec/manager.go index 97c672a4815..18fe5306a22 100644 --- a/services/packages/pkgspec/manager.go +++ b/services/packages/pkgspec/manager.go @@ -6,11 +6,13 @@ package pkgspec import ( packages_model "gitea.dev/models/packages" packages_service "gitea.dev/services/packages" + "gitea.dev/services/packages/debian" "gitea.dev/services/packages/terraform" ) func InitManager() error { mgr := packages_service.GetSpecManager() + mgr.Add(packages_model.TypeDebian, &debian.Specialization{}) mgr.Add(packages_model.TypeTerraformState, &terraform.Specialization{}) // TODO: add more in the future, refactor the existing code to use this approach return nil diff --git a/services/packages/spec.go b/services/packages/spec.go index 3d5888e22f0..19f5eec24dc 100644 --- a/services/packages/spec.go +++ b/services/packages/spec.go @@ -5,27 +5,49 @@ package packages import ( "context" + "html/template" "sync" packages_model "gitea.dev/models/packages" user_model "gitea.dev/models/user" ) -type nop struct{} +type SpecGetViewPackageVersionData struct{} -func (n *nop) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) { +func (*SpecGetViewPackageVersionData) GetViewPackageVersionData(ctx context.Context, pd *packages_model.PackageDescriptor) (any, error) { return nil, nil //nolint:nilnil // no data, no error } -func (n *nop) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error { +type SpecOnBeforeRemovePackageAll struct{} + +func (*SpecOnBeforeRemovePackageAll) OnBeforeRemovePackageAll(ctx context.Context, doer *user_model.User, pkg *packages_model.Package, pds []*packages_model.PackageDescriptor) error { return nil } -func (n *nop) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error { +type SpecOnBeforeRemovePackageVersion struct{} + +func (*SpecOnBeforeRemovePackageVersion) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error { return nil } -var _ Specialization = (*nop)(nil) +type SpecRenderUsageManual struct{} + +func (*SpecRenderUsageManual) RenderSetupManual(ctx context.Context, pkg *packages_model.PackageDescriptor, viewData any) template.HTML { + return "" +} + +type specDefault struct { + SpecGetViewPackageVersionData + SpecOnBeforeRemovePackageAll + SpecOnBeforeRemovePackageVersion + SpecRenderUsageManual +} + +func (n *specDefault) OnBeforeRemovePackageVersion(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) error { + return nil +} + +var _ Specialization = (*specDefault)(nil) type SpecManagerType struct { specMap map[packages_model.Type]Specialization @@ -41,7 +63,7 @@ func (m *SpecManagerType) Get(t packages_model.Type) Specialization { } spec := m.specMap[t] if spec == nil { - return &nop{} + return &specDefault{} } return spec } diff --git a/services/packages/terraform/spec.go b/services/packages/terraform/spec.go index 0aabbb3670b..4f5d49f8fbf 100644 --- a/services/packages/terraform/spec.go +++ b/services/packages/terraform/spec.go @@ -14,7 +14,9 @@ import ( packages_service "gitea.dev/services/packages" ) -type Specialization struct{} +type Specialization struct { + packages_service.SpecRenderUsageManual +} var _ packages_service.Specialization = (*Specialization)(nil) diff --git a/templates/package/content/debian.tmpl b/templates/package/content/debian.tmpl index 7f843effb45..5a2f158ec56 100644 --- a/templates/package/content/debian.tmpl +++ b/templates/package/content/debian.tmpl @@ -4,10 +4,7 @@
-
sudo curl {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc
-echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] {{ctx.AppFullLink}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list
-sudo apt update
-

{{ctx.Locale.Tr "packages.debian.registry.info"}}

+ {{$.PackageVersionSetupManual}}
@@ -21,21 +18,22 @@ sudo apt update
+ {{$viewData := $.PackageVersionViewData}}

{{ctx.Locale.Tr "packages.debian.repository"}}

- + - + - +
{{ctx.Locale.Tr "packages.debian.repository.distributions"}}
{{StringUtils.Join .Distributions ", "}}{{StringUtils.Join $viewData.Distributions ", "}}
{{ctx.Locale.Tr "packages.debian.repository.components"}}
{{StringUtils.Join .Components ", "}}{{StringUtils.Join $viewData.Components ", "}}
{{ctx.Locale.Tr "packages.debian.repository.architectures"}}
{{StringUtils.Join .Architectures ", "}}{{StringUtils.Join $viewData.Architectures ", "}}
diff --git a/tests/integration/api_packages_debian_test.go b/tests/integration/api_packages_debian_test.go index 76b59199d39..e4c7e178759 100644 --- a/tests/integration/api_packages_debian_test.go +++ b/tests/integration/api_packages_debian_test.go @@ -58,6 +58,10 @@ func TestPackageDebian(t *testing.T) { } aw.WriteHeader(hdr) aw.Write(cbuf.Bytes()) + + // if you'd like to do some tests manually, uncomment the WriteFile line, then you will get some "deb" package files: + // OWNER= PASS= DIST= COMP= ; curl -X PUT "http://$OWNER:$PASS@localhost:3000/api/packages/$OWNER/debian/pool/$DIST/$COMP/upload" -T /tmp/gitea_test_debian_gitea_1.0.3_amd64.deb + // _ = os.WriteFile("/tmp/gitea_test_debian_"+name+"_"+version+"_"+architecture+".deb", buf.Bytes(), 0o644) return &buf } diff --git a/web_src/css/repo/packages.css b/web_src/css/repo/packages.css index 75675f52430..90ed4477cb9 100644 --- a/web_src/css/repo/packages.css +++ b/web_src/css/repo/packages.css @@ -9,6 +9,24 @@ width: calc(100% - 250px - 16px); } +.packages-content-left .markup { + margin: var(--gap-block) 0; +} + +.packages-content-left .markup pre.code-block { + margin: 0; +} + +.package-manual-vars { + display: flex; + gap: 1em; + flex-wrap: wrap; +} + +.package-manual-vars select { + border-radius: var(--border-radius); +} + .packages-content-right { margin: 0 !important; width: 250px; diff --git a/web_src/js/features/packages.test.ts b/web_src/js/features/packages.test.ts new file mode 100644 index 00000000000..ef6f124a3ad --- /dev/null +++ b/web_src/js/features/packages.test.ts @@ -0,0 +1,18 @@ +import {initPackagesManualVars} from './packages.ts'; +import {createElementFromHTML} from '../utils/dom.ts'; + +test('initPackagesManualVars', () => { + const el = createElementFromHTML(` +
+ + $foo +
+`); + initPackagesManualVars(el); + const elSelect = el.querySelector('select')!; + const elSpan = el.querySelector('span')!; + expect(elSpan.textContent).toBe('v1'); + elSelect.value = 'v2'; + elSelect.dispatchEvent(new Event('change')); + expect(elSpan.textContent).toBe('v2'); +}); diff --git a/web_src/js/features/packages.ts b/web_src/js/features/packages.ts new file mode 100644 index 00000000000..e1f442382e0 --- /dev/null +++ b/web_src/js/features/packages.ts @@ -0,0 +1,24 @@ +import {registerGlobalInitFunc} from '../modules/observer.ts'; +import {queryElems} from '../utils/dom.ts'; + +export function initPackagesManualVars(el: HTMLElement) { + queryElems(el, 'span', (elSpan: HTMLElement) => { + const text = elSpan.textContent; + if (!text.startsWith('$')) return; + elSpan.setAttribute('data-code-var', text.substring(1)); + }); + const syncVar = (elSelect: HTMLSelectElement) => { + const varName = elSelect.getAttribute('data-code-var')!; + const elSpans = el.querySelectorAll(`span[data-code-var="${CSS.escape(varName)}"]`); + if (elSpans.length === 0) throw new Error(`No span found for variable ${varName}`); + for (const elSpan of elSpans) elSpan.textContent = elSelect.value; + }; + queryElems(el, 'select[data-code-var]', (elSelect: HTMLSelectElement) => { + syncVar(elSelect); + elSelect.addEventListener('change', () => syncVar(elSelect)); + }); +} + +export function initPackagesView() { + registerGlobalInitFunc('initPackagesManualVars', initPackagesManualVars); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index c7bbace3bcf..41841c498a8 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -66,6 +66,7 @@ import {initRefIssueContextPopup} from './features/ref-issue.ts'; import {initGlobalShortcut} from './modules/shortcut.ts'; import {initDevtest} from './modules/devtest.ts'; import {initRepoWatch} from './features/repo-watch.ts'; +import {initPackagesView} from './features/packages.ts'; const initStartTime = performance.now(); const initPerformanceTracer = callInitFunctions([ @@ -103,6 +104,7 @@ const initPerformanceTracer = callInitFunctions([ initTableSort, initRepoFileSearch, initCopyContent, + initPackagesView, initAdminCommon, initAdminUserListSearchForm,