diff options
57 files changed, 1212 insertions, 597 deletions
diff --git a/.dockerignore b/.dockerignore index 94aca6b8d3..843f12a7be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -36,15 +36,6 @@ _testmain.go coverage.all cpu.out -/modules/migration/bindata.go -/modules/migration/bindata.go.hash -/modules/options/bindata.go -/modules/options/bindata.go.hash -/modules/public/bindata.go -/modules/public/bindata.go.hash -/modules/templates/bindata.go -/modules/templates/bindata.go.hash - *.db *.log @@ -1,9 +1,6 @@ *.min.css *.min.js /assets/*.json -/modules/options/bindata.go -/modules/public/bindata.go -/modules/templates/bindata.go /options/gitignore /options/license /public/assets @@ -120,7 +120,7 @@ WEBPACK_CONFIGS := webpack.config.js tailwind.config.js WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts -BINDATA_DEST := modules/public/bindata.dat modules/options/bindata.dat modules/templates/bindata.dat +BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.* GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go @@ -219,7 +219,7 @@ clean-all: clean ## delete backend, frontend and integration files .PHONY: clean clean: ## delete backend and integration files - rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) \ + rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \ integrations*.test \ e2e*.test \ tests/integration/gitea-integration-* \ @@ -91,7 +91,7 @@ require ( github.com/minio/minio-go/v7 v7.0.91 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.63 - github.com/niklasfasching/go-org v1.7.0 + github.com/niklasfasching/go-org v1.8.0 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 @@ -551,8 +551,8 @@ github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= -github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= +github.com/niklasfasching/go-org v1.8.0 h1:WyGLaajLLp8JbQzkmapZ1y0MOzKuKV47HkZRloi+HGY= +github.com/niklasfasching/go-org v1.8.0/go.mod h1:e2A9zJs7cdONrEGs3gvxCcaAEpwwPNPG7csDpXckMNg= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index 6536e1dda7..03e21d04b4 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -201,3 +201,15 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 25 + repo_id: 54 + name: 'master' + commit_id: '73cf03db6ece34e12bf91e8853dc58f678f2f82d' + commit_message: 'Initial commit' + commit_time: 1671663402 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/models/issues/pull.go b/models/issues/pull.go index e65b214dab..0ff32e2473 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -649,12 +649,6 @@ func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*P return pulls, err } -// Update updates all fields of pull request. -func (pr *PullRequest) Update(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(pr.ID).AllCols().Update(pr) - return err -} - // UpdateCols updates specific fields of pull request. func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error { _, err := db.GetEngine(ctx).ID(pr.ID).Cols(cols...).Update(pr) diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 53898cb42e..39efaa5792 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -248,19 +248,6 @@ func TestGetPullRequestByIssueID(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestPullRequest_Update(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - pr.BaseBranch = "baseBranch" - pr.HeadBranch = "headBranch" - pr.Update(db.DefaultContext) - - pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) - assert.Equal(t, "baseBranch", pr.BaseBranch) - assert.Equal(t, "headBranch", pr.HeadBranch) - unittest.CheckConsistencyFor(t, pr) -} - func TestPullRequest_UpdateCols(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := &issues_model.PullRequest{ diff --git a/models/packages/container/search.go b/models/packages/container/search.go index 5df35117ce..9321d9eb41 100644 --- a/models/packages/container/search.go +++ b/models/packages/container/search.go @@ -25,6 +25,7 @@ type BlobSearchOptions struct { Digest string Tag string IsManifest bool + OnlyLead bool Repository string } @@ -43,7 +44,10 @@ func (opts *BlobSearchOptions) toConds() builder.Cond { cond = cond.And(builder.Eq{"package_version.lower_version": strings.ToLower(opts.Tag)}) } if opts.IsManifest { - cond = cond.And(builder.Eq{"package_file.lower_name": ManifestFilename}) + cond = cond.And(builder.Eq{"package_file.lower_name": container_module.ManifestFilename}) + } + if opts.OnlyLead { + cond = cond.And(builder.Eq{"package_file.is_lead": true}) } if opts.Digest != "" { var propsCond builder.Cond = builder.Eq{ @@ -73,11 +77,9 @@ func GetContainerBlob(ctx context.Context, opts *BlobSearchOptions) (*packages.P pfds, err := getContainerBlobsLimit(ctx, opts, 1) if err != nil { return nil, err - } - if len(pfds) != 1 { + } else if len(pfds) == 0 { return nil, ErrContainerBlobNotExist } - return pfds[0], nil } @@ -233,7 +235,7 @@ func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*pack func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([]*packages.PackageFile, error) { var cond builder.Cond = builder.Eq{ "package_version.is_internal": true, - "package_version.lower_version": UploadVersion, + "package_version.lower_version": container_module.UploadVersion, "package.type": packages.TypeContainer, } cond = cond.And(builder.Lt{"package_file.created_unix": time.Now().Add(-olderThan).Unix()}) diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 1ea181c723..2d43dc3046 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -103,10 +103,10 @@ func (pd *PackageDescriptor) CalculateBlobSize() int64 { // GetPackageDescriptor gets the package description for a version func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDescriptor, error) { - return getPackageDescriptor(ctx, pv, cache.NewEphemeralCache()) + return GetPackageDescriptorWithCache(ctx, pv, cache.NewEphemeralCache()) } -func getPackageDescriptor(ctx context.Context, pv *PackageVersion, c *cache.EphemeralCache) (*PackageDescriptor, error) { +func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *cache.EphemeralCache) (*PackageDescriptor, error) { p, err := cache.GetWithEphemeralCache(ctx, c, "package", pv.PackageID, GetPackageByID) if err != nil { return nil, err @@ -270,7 +270,7 @@ func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*Packa func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) { pds := make([]*PackageDescriptor, 0, len(pvs)) for _, pv := range pvs { - pd, err := getPackageDescriptor(ctx, pv, c) + pd, err := GetPackageDescriptorWithCache(ctx, pv, c) if err != nil { return nil, err } diff --git a/models/packages/package_file.go b/models/packages/package_file.go index 270cb32fdf..bf877485d6 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -115,6 +115,11 @@ func DeleteFileByID(ctx context.Context, fileID int64) error { return err } +func UpdateFile(ctx context.Context, pf *PackageFile, cols []string) error { + _, err := db.GetEngine(ctx).ID(pf.ID).Cols(cols...).Update(pf) + return err +} + // PackageFileSearchOptions are options for SearchXXX methods type PackageFileSearchOptions struct { OwnerID int64 diff --git a/models/packages/package_property.go b/models/packages/package_property.go index e0170016cf..10670951ad 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -66,6 +66,20 @@ func UpdateProperty(ctx context.Context, pp *PackageProperty) error { return err } +func InsertOrUpdateProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) error { + pp := PackageProperty{RefType: refType, RefID: refID, Name: name} + ok, err := db.GetEngine(ctx).Get(&pp) + if err != nil { + return err + } + if ok { + _, err = db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", refType, refID, name).Cols("value").Update(&PackageProperty{Value: value}) + return err + } + _, err = InsertProperty(ctx, refType, refID, name, value) + return err +} + // DeleteAllProperties deletes all properties of a ref func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) error { _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Delete(&PackageProperty{}) diff --git a/models/repo/release.go b/models/repo/release.go index 06cfa37342..59f4caf5aa 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -180,7 +180,7 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs } attachments[i].ReleaseID = releaseID // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { + if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("release_id").Update(attachments[i]); err != nil { return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) } } diff --git a/models/repo/update.go b/models/repo/update.go index 8a15477a80..f82ff7c76c 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -42,12 +42,18 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t // UpdateRepositoryColsWithAutoTime updates repository's columns func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error { + if len(cols) == 0 { + return nil + } _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo) return err } // UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error { + if len(cols) == 0 { + return nil + } _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo) return err } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 7a6af0410b..9368077365 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -7,8 +7,6 @@ package git import ( "context" - "fmt" - "io" "path" "sort" @@ -124,48 +122,25 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, return nil, err } - batchStdinWriter, batchReader, cancel, err := commit.repo.CatFileBatch(ctx) - if err != nil { - return nil, err - } - defer cancel() - commitsMap := map[string]*Commit{} commitsMap[commit.ID.String()] = commit commitCommits := map[string]*Commit{} for path, commitID := range revs { - c, ok := commitsMap[commitID] - if ok { - commitCommits[path] = c + if len(commitID) == 0 { continue } - if len(commitID) == 0 { + c, ok := commitsMap[commitID] + if ok { + commitCommits[path] = c continue } - _, err := batchStdinWriter.Write([]byte(commitID + "\n")) - if err != nil { - return nil, err - } - _, typ, size, err := ReadBatchLine(batchReader) + c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository if err != nil { return nil, err } - if typ != "commit" { - if err := DiscardFull(batchReader, size+1); err != nil { - return nil, err - } - return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID) - } - c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size)) - if err != nil { - return nil, err - } - if _, err := batchReader.Discard(1); err != nil { - return nil, err - } commitCommits[path] = c } diff --git a/models/packages/container/const.go b/modules/packages/container/const.go index 0dfbda051d..6c7c9b46d1 100644 --- a/models/packages/container/const.go +++ b/modules/packages/container/const.go @@ -4,6 +4,8 @@ package container const ( + ContentTypeDockerDistributionManifestV2 = "application/vnd.docker.distribution.manifest.v2+json" + ManifestFilename = "manifest.json" UploadVersion = "_upload" ) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 1e98ddffde..a122590bf1 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -57,14 +57,24 @@ type Package struct { // Metadata represents the metadata of a Nuget package type Metadata struct { - Description string `json:"description,omitempty"` - ReleaseNotes string `json:"release_notes,omitempty"` - Readme string `json:"readme,omitempty"` - Authors string `json:"authors,omitempty"` - ProjectURL string `json:"project_url,omitempty"` - RepositoryURL string `json:"repository_url,omitempty"` - RequireLicenseAcceptance bool `json:"require_license_acceptance"` - Dependencies map[string][]Dependency `json:"dependencies,omitempty"` + Authors string `json:"authors,omitempty"` + Copyright string `json:"copyright,omitempty"` + Description string `json:"description,omitempty"` + DevelopmentDependency bool `json:"development_dependency,omitempty"` + IconURL string `json:"icon_url,omitempty"` + Language string `json:"language,omitempty"` + LicenseURL string `json:"license_url,omitempty"` + MinClientVersion string `json:"min_client_version,omitempty"` + Owners string `json:"owners,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Readme string `json:"readme,omitempty"` + ReleaseNotes string `json:"release_notes,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + RequireLicenseAcceptance bool `json:"require_license_acceptance"` + Tags string `json:"tags,omitempty"` + Title string `json:"title,omitempty"` + + Dependencies map[string][]Dependency `json:"dependencies,omitempty"` } // Dependency represents a dependency of a Nuget package @@ -74,24 +84,30 @@ type Dependency struct { } // https://learn.microsoft.com/en-us/nuget/reference/nuspec +// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Packaging/compiler/resources/nuspec.xsd type nuspecPackage struct { Metadata struct { - ID string `xml:"id"` - Version string `xml:"version"` - Authors string `xml:"authors"` - RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` + // required fields + Authors string `xml:"authors"` + Description string `xml:"description"` + ID string `xml:"id"` + Version string `xml:"version"` + + // optional fields + Copyright string `xml:"copyright"` + DevelopmentDependency bool `xml:"developmentDependency"` + IconURL string `xml:"iconUrl"` + Language string `xml:"language"` + LicenseURL string `xml:"licenseUrl"` + MinClientVersion string `xml:"minClientVersion,attr"` + Owners string `xml:"owners"` ProjectURL string `xml:"projectUrl"` - Description string `xml:"description"` - ReleaseNotes string `xml:"releaseNotes"` Readme string `xml:"readme"` - PackageTypes struct { - PackageType []struct { - Name string `xml:"name,attr"` - } `xml:"packageType"` - } `xml:"packageTypes"` - Repository struct { - URL string `xml:"url,attr"` - } `xml:"repository"` + ReleaseNotes string `xml:"releaseNotes"` + RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` + Tags string `xml:"tags"` + Title string `xml:"title"` + Dependencies struct { Dependency []struct { ID string `xml:"id,attr"` @@ -107,6 +123,14 @@ type nuspecPackage struct { } `xml:"dependency"` } `xml:"group"` } `xml:"dependencies"` + PackageTypes struct { + PackageType []struct { + Name string `xml:"name,attr"` + } `xml:"packageType"` + } `xml:"packageTypes"` + Repository struct { + URL string `xml:"url,attr"` + } `xml:"repository"` } `xml:"metadata"` } @@ -167,13 +191,23 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { } m := &Metadata{ - Description: p.Metadata.Description, - ReleaseNotes: p.Metadata.ReleaseNotes, Authors: p.Metadata.Authors, + Copyright: p.Metadata.Copyright, + Description: p.Metadata.Description, + DevelopmentDependency: p.Metadata.DevelopmentDependency, + IconURL: p.Metadata.IconURL, + Language: p.Metadata.Language, + LicenseURL: p.Metadata.LicenseURL, + MinClientVersion: p.Metadata.MinClientVersion, + Owners: p.Metadata.Owners, ProjectURL: p.Metadata.ProjectURL, + ReleaseNotes: p.Metadata.ReleaseNotes, RepositoryURL: p.Metadata.Repository.URL, RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, - Dependencies: make(map[string][]Dependency), + Tags: p.Metadata.Tags, + Title: p.Metadata.Title, + + Dependencies: make(map[string][]Dependency), } if p.Metadata.Readme != "" { @@ -227,13 +261,13 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { func toNormalizedVersion(v *version.Version) string { var buf bytes.Buffer segments := v.Segments64() - fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) + _, _ = fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) if len(segments) > 3 && segments[3] > 0 { - fmt.Fprintf(&buf, ".%d", segments[3]) + _, _ = fmt.Fprintf(&buf, ".%d", segments[3]) } pre := v.Prerelease() if pre != "" { - fmt.Fprint(&buf, "-", pre) + _, _ = fmt.Fprint(&buf, "-", pre) } return buf.String() } diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go index f466492f8a..90c3e8dfeb 100644 --- a/modules/packages/nuget/metadata_test.go +++ b/modules/packages/nuget/metadata_test.go @@ -12,44 +12,62 @@ import ( ) const ( - id = "System.Gitea" - semver = "1.0.1" - authors = "Gitea Authors" - projectURL = "https://gitea.io" - description = "Package Description" - releaseNotes = "Package Release Notes" - readme = "Readme" - repositoryURL = "https://gitea.io/gitea/gitea" - targetFramework = ".NETStandard2.1" - dependencyID = "System.Text.Json" - dependencyVersion = "5.0.0" + authors = "Gitea Authors" + copyright = "Package Copyright" + dependencyID = "System.Text.Json" + dependencyVersion = "5.0.0" + developmentDependency = true + description = "Package Description" + iconURL = "https://gitea.io/favicon.png" + id = "System.Gitea" + language = "Package Language" + licenseURL = "https://gitea.io/license" + minClientVersion = "1.0.0.0" + owners = "Package Owners" + projectURL = "https://gitea.io" + readme = "Readme" + releaseNotes = "Package Release Notes" + repositoryURL = "https://gitea.io/gitea/gitea" + requireLicenseAcceptance = true + tags = "tag_1 tag_2 tag_3" + targetFramework = ".NETStandard2.1" + title = "Package Title" + versionStr = "1.0.1" ) const nuspecContent = `<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> - <metadata> - <id>` + id + `</id> - <version>` + semver + `</version> - <authors>` + authors + `</authors> - <requireLicenseAcceptance>true</requireLicenseAcceptance> - <projectUrl>` + projectURL + `</projectUrl> - <description>` + description + `</description> - <releaseNotes>` + releaseNotes + `</releaseNotes> - <repository url="` + repositoryURL + `" /> - <readme>README.md</readme> - <dependencies> - <group targetFramework="` + targetFramework + `"> - <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" /> - </group> - </dependencies> - </metadata> + <metadata minClientVersion="` + minClientVersion + `"> + <authors>` + authors + `</authors> + <copyright>` + copyright + `</copyright> + <description>` + description + `</description> + <developmentDependency>true</developmentDependency> + <iconUrl>` + iconURL + `</iconUrl> + <id>` + id + `</id> + <language>` + language + `</language> + <licenseUrl>` + licenseURL + `</licenseUrl> + <owners>` + owners + `</owners> + <projectUrl>` + projectURL + `</projectUrl> + <readme>README.md</readme> + <releaseNotes>` + releaseNotes + `</releaseNotes> + <repository url="` + repositoryURL + `" /> + <requireLicenseAcceptance>true</requireLicenseAcceptance> + <tags>` + tags + `</tags> + <title>` + title + `</title> + <version>` + versionStr + `</version> + <dependencies> + <group targetFramework="` + targetFramework + `"> + <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" /> + </group> + </dependencies> + </metadata> </package>` const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <metadata> <id>` + id + `</id> - <version>` + semver + `</version> + <version>` + versionStr + `</version> <description>` + description + `</description> <packageTypes> <packageType name="SymbolsPackage" /> @@ -140,14 +158,26 @@ func TestParsePackageMetaData(t *testing.T) { assert.NotNil(t, np) assert.Equal(t, DependencyPackage, np.PackageType) - assert.Equal(t, id, np.ID) - assert.Equal(t, semver, np.Version) assert.Equal(t, authors, np.Metadata.Authors) - assert.Equal(t, projectURL, np.Metadata.ProjectURL) assert.Equal(t, description, np.Metadata.Description) - assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes) + assert.Equal(t, id, np.ID) + assert.Equal(t, versionStr, np.Version) + + assert.Equal(t, copyright, np.Metadata.Copyright) + assert.Equal(t, developmentDependency, np.Metadata.DevelopmentDependency) + assert.Equal(t, iconURL, np.Metadata.IconURL) + assert.Equal(t, language, np.Metadata.Language) + assert.Equal(t, licenseURL, np.Metadata.LicenseURL) + assert.Equal(t, minClientVersion, np.Metadata.MinClientVersion) + assert.Equal(t, owners, np.Metadata.Owners) + assert.Equal(t, projectURL, np.Metadata.ProjectURL) assert.Equal(t, readme, np.Metadata.Readme) + assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes) assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL) + assert.Equal(t, requireLicenseAcceptance, np.Metadata.RequireLicenseAcceptance) + assert.Equal(t, tags, np.Metadata.Tags) + assert.Equal(t, title, np.Metadata.Title) + assert.Len(t, np.Metadata.Dependencies, 1) assert.Contains(t, np.Metadata.Dependencies, targetFramework) deps := np.Metadata.Dependencies[targetFramework] @@ -180,7 +210,7 @@ func TestParsePackageMetaData(t *testing.T) { assert.Equal(t, SymbolsPackage, np.PackageType) assert.Equal(t, id, np.ID) - assert.Equal(t, semver, np.Version) + assert.Equal(t, versionStr, np.Version) assert.Equal(t, description, np.Metadata.Description) assert.Empty(t, np.Metadata.Dependencies) }) diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 81bf0371a0..9c952e1f10 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -34,7 +34,7 @@ type PortablePdbList []*PortablePdb func (l PortablePdbList) Close() { for _, pdb := range l { - pdb.Content.Close() + _ = pdb.Content.Close() } } @@ -65,7 +65,7 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { buf, err := packages.CreateHashedBufferFromReader(f) - f.Close() + _ = f.Close() if err != nil { return err @@ -73,12 +73,12 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { id, err := ParseDebugHeaderID(buf) if err != nil { - buf.Close() + _ = buf.Close() return fmt.Errorf("Invalid PDB file: %w", err) } if _, err := buf.Seek(0, io.SeekStart); err != nil { - buf.Close() + _ = buf.Close() return err } diff --git a/modules/packages/nuget/symbol_extractor_test.go b/modules/packages/nuget/symbol_extractor_test.go index 711ad6d096..e841e377d9 100644 --- a/modules/packages/nuget/symbol_extractor_test.go +++ b/modules/packages/nuget/symbol_extractor_test.go @@ -24,14 +24,14 @@ func TestExtractPortablePdb(t *testing.T) { var buf bytes.Buffer archive := zip.NewWriter(&buf) w, _ := archive.Create(name) - w.Write(content) - archive.Close() + _, _ = w.Write(content) + _ = archive.Close() return buf.Bytes() } t.Run("MissingPdbFiles", func(t *testing.T) { var buf bytes.Buffer - zip.NewWriter(&buf).Close() + _ = zip.NewWriter(&buf).Close() pdbs, err := ExtractPortablePdb(bytes.NewReader(buf.Bytes()), int64(buf.Len())) assert.ErrorIs(t, err, ErrMissingPdbFiles) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8797ef4bc0..6d8aaef4cd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1332,7 +1332,9 @@ editor.upload_file = Upload File editor.edit_file = Edit File editor.preview_changes = Preview Changes editor.cannot_edit_lfs_files = LFS files cannot be edited in the web interface. +editor.cannot_edit_too_large_file = The file is too large to be edited. editor.cannot_edit_non_text_files = Binary files cannot be edited in the web interface. +editor.file_not_editable_hint = But you can still rename or move it. editor.edit_this_file = Edit File editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 68f5e42d47..c9ebee3792 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -156,7 +156,7 @@ filter.is_mirror=Віддзеркалено filter.not_mirror=Ðе віддзеркалено filter.is_template=Шаблон filter.not_template=Ðе шаблон -filter.public=Публічний +filter.public=Публічна filter.private=Приватний no_results_found=Ðічого не знайдено. @@ -1848,15 +1848,15 @@ settings.admin_stats_indexer=ІндекÑатор ÑтатиÑтики коду settings.admin_indexer_commit_sha=ОÑтанній індекÑований SHA settings.admin_indexer_unindexed=Ðе індекÑовано settings.reindex_button=Додати до черги на реіндекÑацію -settings.reindex_requested=Запит на реіндекÑацію +settings.reindex_requested=Запит на переіндекÑацію settings.admin_enable_close_issues_via_commit_in_any_branch=Закрити задачу за допомогою коміта, зробленого не в головній гілці settings.danger_zone=Ðебезпечна зона settings.new_owner_has_same_repo=Ðовий влаÑник вже має Ñховище з такою назвою. Будь лаÑка, виберіть іншу назву. settings.convert=Перетворити на звичайне Ñховище -settings.convert_desc=Ви можете Ñконвертувати це дзеркало у звичайний репозиторій. Це не може бути ÑкаÑовано. +settings.convert_desc=Ви можете перетворити це дзеркало на звичайне Ñховище. Це неможливо ÑкаÑувати. settings.convert_notices_1=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¿ÐµÑ€ÐµÑ‚Ð²Ð¾Ñ€Ð¸Ñ‚ÑŒ дзеркало у звичайний репозиторій Ñ– не може бути ÑкаÑована. settings.convert_confirm=Перетворити репозиторій -settings.convert_succeed=Репозиторій уÑпішно перетворений в звичайний. +settings.convert_succeed=Дзеркало було перетворено на звичайне Ñховище. settings.convert_fork=Перетворити на звичайний репозиторій settings.convert_fork_desc=Ви можете перетворити цей форк на звичайний репозиторій. Цю дію неможливо ÑкаÑувати. settings.convert_fork_notices_1=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¿ÐµÑ€ÐµÑ‚Ð²Ð¾Ñ€Ð¸Ñ‚ÑŒ форк на звичайний репозиторій та не може бути ÑкаÑованою. @@ -1866,89 +1866,88 @@ settings.transfer=Передати новому влаÑнику settings.transfer.rejected=ПеренеÑÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ відхилено. settings.transfer.success=ПеренеÑÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ виконано. settings.transfer_abort=СкаÑувати перенеÑÐµÐ½Ð½Ñ -settings.transfer_abort_invalid=Ви не можете ÑкаÑувати неіÑнуюче перенеÑÐµÐ½Ð½Ñ Ñховища. -settings.transfer_desc=Передати репозиторій кориÑтувачеві або організації, де ви маєте права адмініÑтратора. -settings.transfer_form_title=Введіть ім'Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ñк підтвердженнÑ: -settings.transfer_in_progress=Ð’ даний Ñ‡Ð°Ñ Ð²Ñ–Ð´Ð±ÑƒÐ²Ð°Ñ”Ñ‚ÑŒÑÑ Ð¿ÐµÑ€ÐµÐ½ÐµÑеннÑ. Будь лаÑка, ÑкаÑуйте його, Ñкщо ви бажаєте перенеÑти цей репозиторій іншому кориÑтувачу. -settings.transfer_notices_1=- Ви втратите доÑтуп до репозиторіÑ, Ñкщо ви переведете його окремому кориÑтувачеві. -settings.transfer_notices_2=- Ви збережете доÑтуп, Ñкщо новим влаÑником Ñтане організаціÑ, влаÑником Ñкої ви Ñ”. -settings.transfer_notices_3=- Якщо репозиторій Ñ” приватним Ñ– передаєтьÑÑ Ð¾ÐºÑ€ÐµÐ¼Ð¾Ð¼Ñƒ кориÑтувачеві, Ñ†Ñ Ð´Ñ–Ñ Ð³Ð°Ñ€Ð°Ð½Ñ‚ÑƒÑ”, що кориÑтувач має хоча б дозвіл на Ñ‡Ð¸Ñ‚Ð°Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð°Ñ€Ñ–ÑŽ (Ñ– при необхідноÑті змінює права дозволів). +settings.transfer_abort_invalid=Ви не можете ÑкаÑувати перенеÑÐµÐ½Ð½Ñ Ð½ÐµÑ–Ñнуючого Ñховища. +settings.transfer_desc=Передати це Ñховище кориÑтувачеві або організації, Ð´Ð»Ñ Ñкої ви маєте права адмініÑтратора. +settings.transfer_form_title=Введіть назву Ñховища Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ: +settings.transfer_in_progress=Ðаразі триває передача. Будь лаÑка, ÑкаÑуйте його, Ñкщо ви хочете передати це Ñховище іншому кориÑтувачеві. +settings.transfer_notices_1=- Ви втратите доÑтуп до Ñховища, Ñкщо передаÑте його окремому кориÑтувачеві. +settings.transfer_notices_2=- Ви збережете доÑтуп до Ñховища, Ñкщо передаÑте його організації, Ñкою ви (Ñпів)володієте. +settings.transfer_notices_3=- Якщо Ñховище Ñ” приватним Ñ– передаєтьÑÑ Ð¾ÐºÑ€ÐµÐ¼Ð¾Ð¼Ñƒ кориÑтувачеві, Ñ†Ñ Ð´Ñ–Ñ Ð³Ð°Ñ€Ð°Ð½Ñ‚ÑƒÑ”, що кориÑтувач має принаймні права на Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ (Ñ– змінює ці права, Ñкщо необхідно). settings.transfer_owner=Ðовий влаÑник settings.transfer_perform=ЗдіÑнити перенеÑÐµÐ½Ð½Ñ -settings.transfer_started=`Цей репозиторій чекає Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÐµÐ½ÐµÑÐµÐ½Ð½Ñ Ð²Ñ–Ð´ "%s"` -settings.transfer_succeed=Репозиторій був перенеÑений. +settings.transfer_started=Це Ñховище було позначено Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ñ– та очікує на Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ «%s» +settings.transfer_succeed=Сховище перенеÑено. settings.signing_settings=Параметри перевірки підпиÑу -settings.trust_model=Модель довіри Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу -settings.trust_model.default=Модель довіри за замовчуваннÑм -settings.trust_model.default.desc=ВикориÑтовувати модель довіри репозиторію за замовчуваннÑм Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñайту. +settings.trust_model=Модель довіри до підпиÑу +settings.trust_model.default=Типова модель довіри +settings.trust_model.default.desc=ВикориÑтовувати типову модель довіри до Ñховища Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñайту. settings.trust_model.collaborator=Співавтор settings.trust_model.collaborator.long=Співавтор: підпиÑи довіри від Ñпівавторів -settings.trust_model.collaborator.desc=ДопуÑтимі підпиÑи Ñпівавторів цього репозиторію буде позначано Ñк "довірені" - (Ñкщо вони відповідають комітеру чи ні). Ð’ іншому випадку дійÑні підпиÑи будуть позначені Ñк «ненадійні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ñпівпадає з комітером Ñ– «невідповідні», Ñкщо ні. -settings.trust_model.committer=Коммітер -settings.trust_model.committer.long=Коммітер: ДовірÑти підпиÑам Ñкі відповідають комітерам (Так Ñк Ñ– на GitHub, Ñ– змуÑить підпиÑати коміти Gitea в ÑкоÑті коммітера) -settings.trust_model.collaboratorcommitter=Співавтор+Коммітер -settings.trust_model.collaboratorcommitter.long=Співавтор+Коммітер: ДовірÑти підпиÑам від Ñпівавторів, Ñкі відповідають комітеру -settings.trust_model.collaboratorcommitter.desc=ДопуÑтимі підпиÑи Ñпівавторів цього репозиторію будуть позначатиÑÑ Ñк "довірені", Ñкщо вони відповідають комітеру. Ð’ іншому випадку дійÑні підпиÑи будуть позначені Ñк «ненадійні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ñпівпадає з комітером Ñ– Ñк «невідповіді» в іншому випадку. Це змуÑить Gitea бути відміченим Ñк комітер піÑÐ»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑÐ°Ð½Ð½Ñ Ñ„Ð°ÐºÑ‚Ð¸Ñ‡Ð½Ð¸Ð¼ комітером, позначеним Co-Authored-By: Ñ– Co-Committed-By: прикріпленим до комміту. Типовий ключ Gitea повинен відповідати кориÑтувачу в базі даних. -settings.wiki_delete=Видалити вікі-дані -settings.wiki_delete_desc=Будьте уважні! Як тільки ви видалите Вікі - шлÑху назад не буде. -settings.wiki_delete_notices_1=- Це назавжди знищить Ñ– відключить wiki Ð´Ð»Ñ %s. -settings.confirm_wiki_delete=Видалити Вікі-дані -settings.wiki_deletion_success=Дані wiki були видалені. +settings.trust_model.collaborator.desc=ДійÑні підпиÑи Ñпівавторів цього Ñховища будуть позначені Ñк «довірені» - (незалежно від того, чи збігаютьÑÑ Ð²Ð¾Ð½Ð¸ з підпиÑом комітера чи ні). Ð’ іншому випадку дійÑні підпиÑи будуть позначені Ñк «недійÑні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð·Ð±Ñ–Ð³Ð°Ñ”Ñ‚ÑŒÑÑ Ð· комітером Ñ– «невідповідні», Ñкщо ні. +settings.trust_model.committer=Комітер +settings.trust_model.committer.long=Комітер: ДовірÑти підпиÑам, Ñкі відповідають комітерам (це відповідає GitHub Ñ– змуÑить підпиÑані Gitea коміти мати Gitea в ÑкоÑті комітера) +settings.trust_model.collaboratorcommitter=Співавтор+Комітер +settings.trust_model.collaboratorcommitter.long=Співавтор+Комітер: ДовірÑти підпиÑам від Ñпівавторів, Ñкі відповідають комітеру +settings.trust_model.collaboratorcommitter.desc=ДійÑні підпиÑи Ñпівавторів цього Ñховища будуть позначені Ñк «довірені», Ñкщо вони збігаютьÑÑ Ð· комітером. Ð’ іншому випадку, дійÑні підпиÑи будуть позначені Ñк «недійÑні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð·Ð±Ñ–Ð³Ð°Ñ”Ñ‚ÑŒÑÑ Ð· комітером, Ñ– «невідповідні» у протилежному випадку. Це призведе до того, що Gitea буде позначено комітером у підпиÑаних комітах, а Ñправжній комітер буде позначений Ñк Co-Author-By: та Co-Committed-By: у трейлері коміта. Типовий ключ Gitea має відповідати кориÑтувачеві у базі даних. +settings.wiki_delete=Видалити дані Вікі +settings.wiki_delete_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… Вікі Ñховища Ñ” оÑтаточним Ñ– не може бути ÑкаÑоване. +settings.wiki_delete_notices_1=- Це назавжди видалить Ñ– вимкне вікі Ñховища Ð´Ð»Ñ %s. +settings.confirm_wiki_delete=Видалити дані Вікі +settings.wiki_deletion_success=Дані Вікі видалено. settings.delete=Видалити цей репозиторій -settings.delete_desc=Будьте уважні! Як тільки ви видалите репозиторій - шлÑху назад не буде. -settings.delete_notices_1=- Цю операцію <strong>ÐЕ МОЖÐÐ</strong> відмінити. -settings.delete_notices_2=- Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¾Ñтаточно видалить <strong>%s</strong> репозиторій, включаючи код, задачі, коментарі, вікі та Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñпівавторів. +settings.delete_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñховища Ñ” оÑтаточним Ñ– не може бути ÑкаÑоване. +settings.delete_notices_1=- Цю операцію <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати. +settings.delete_notices_2=- Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить Ñховище <strong>%s</strong>, включно з кодом, проблемами, коментарÑми, даними вікі та налаштуваннÑми Ñпівавторів. settings.delete_notices_fork_1=- Ð’ÑÑ– форки Ñтануть незалежними репозиторіÑми піÑÐ»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ. settings.deletion_success=Репозиторій уÑпішно видалено. -settings.update_settings_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ було оновлено. +settings.update_settings_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñховища оновлено. settings.confirm_delete=Видалити репозиторій settings.add_collaborator=Додати Ñпівавтора settings.add_collaborator_success=Додано Ñпівавтора. -settings.add_collaborator_inactive_user=Ðе можливо додати неактивного кориÑтувача ÑкоÑті Ñпівавтора. +settings.add_collaborator_inactive_user=Ðеможливо додати неактивного кориÑтувача Ñк Ñпівавтора. settings.add_collaborator_duplicate=Співавтора уже додано до цього репозиторію. settings.delete_collaborator=Видалити settings.collaborator_deletion=Видалити Ñпівавтора -settings.collaborator_deletion_desc=Цей кориÑтувач більше не матиме доÑтупу Ð´Ð»Ñ Ñпільної роботи в цьому репозиторії піÑÐ»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ. Ви хочете продовжити? -settings.remove_collaborator_success=Співавтор видалений. +settings.collaborator_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñпівавтора призведе до Ð²Ñ–Ð´ÐºÐ»Ð¸ÐºÐ°Ð½Ð½Ñ Ð¹Ð¾Ð³Ð¾ доÑтупу до цього Ñховища. Продовжити? +settings.remove_collaborator_success=Співавтора видалено. settings.org_not_allowed_to_be_collaborator=Організації не можуть бути додані Ñк Ñпівавтори. settings.change_team_access_not_allowed=Зміна доÑтупу команди до репозитарію обмежена влаÑником організації -settings.team_not_in_organization=Команда та репозитарій мають привÑзки до різних організацій +settings.team_not_in_organization=Команда не належить до тієї ж організації, що й Ñховище settings.teams=Команди -settings.add_team=Додати Команду -settings.add_team_duplicate=Команда вже має привÑзку до репозитарію -settings.add_team_success=Команда отримала доÑтуп до репозиторію. -settings.change_team_permission_tip=Дозволи команди вÑтановлюютьÑÑ Ð½Ð° Ñторінці налаштувань команди та не можуть бути заданими Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ з репозиторіїв окремо +settings.add_team=Додати команду +settings.add_team_duplicate=Команда вже має Ñховище +settings.add_team_success=Команда тепер має доÑтуп до Ñховища. +settings.change_team_permission_tip=Дозвіл команди вÑтановлюєтьÑÑ Ð½Ð° Ñторінці налаштувань команди Ñ– не може бути змінений Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ Ñховища settings.delete_team_tip=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° має доÑтуп до вÑÑ–Ñ… репозиторіїв та не може бути видалена -settings.remove_team_success=ДоÑтуп команди до репозиторію видалений. +settings.remove_team_success=ДоÑтуп команди до Ñховища видалено. settings.add_webhook=Додати веб-хук -settings.add_webhook.invalid_channel_name=Ðазва каналу Webhook не може бути порожньою Ñ– не може міÑтити лише Ñимвол #. -settings.hooks_desc=Веб-хуки автоматично робить HTTP POST-запити на Ñервер, коли відбуваютьÑÑ Ð¿ÐµÐ²Ð½Ñ– події Gitea. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в <a target="_blank" rel="noopener" href="%s"> інÑтрукції по викориÑтанню web-хуків </a>. +settings.add_webhook.invalid_channel_name=Ðазва каналу веб-хука не може бути порожньою Ñ– міÑтити лише Ñимвол #. +settings.hooks_desc=Веб-хуки автоматично роблÑть HTTP POST запити до Ñервера, коли відбуваютьÑÑ Ð¿ÐµÐ²Ð½Ñ– події Gitea. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в <a target="_blank" rel="noopener noreferrer" href="%s">інÑтрукції по викориÑтанню веб-хуків</a>. settings.webhook_deletion=Видалити веб-хук -settings.webhook_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ веб-хука призведе до Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²Ñієї пов'Ñзаної з ним інформації, включаючи Ñ–Ñторію. Бажаєте продовжити? -settings.webhook_deletion_success=Webhook видалено. +settings.webhook_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÐµÐ±-хука видалÑÑ” його Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ‚Ð° Ñ–Ñторію доÑтавки. Продовжити? +settings.webhook_deletion_success=Веб-хук видалено. settings.webhook.test_delivery=Перевірити доÑтавку -settings.webhook.test_delivery_desc=Перевірте цей веб-хук з підробленою подією. +settings.webhook.test_delivery_desc=Перевірте цей веб-хук з фальшивою подією. settings.webhook.request=Запит settings.webhook.response=Відповідь settings.webhook.headers=Заголовки settings.webhook.payload=ЗміÑÑ‚ settings.webhook.body=Тіло -settings.githook_edit_desc=Якщо хук неактивний, буде предÑтавлено зразок зміÑту. Порожнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ñƒ цьому полі призведе до Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð½Ñ Ñ…ÑƒÐºÑƒ. -settings.githook_name=Ім'Ñ Ñ…ÑƒÐºÑƒ +settings.githook_edit_desc=Якщо хук неактивний, буде показано зразок вміÑту. Якщо залишити вміÑÑ‚ порожнім, хук буде вимкнено. +settings.githook_name=Ðазва хуку settings.githook_content=ЗміÑÑ‚ хука settings.update_githook=Оновити хук -settings.add_webhook_desc=Gitea буде відправлÑти <code>POST</code> запити на вказану URL адреÑу, з інформацією про події, що відбуваютьÑÑ. Подробиці на Ñторінці <a target="_blank" rel="noopener" href="%s"> інÑтрукції по викориÑтанню web-хуків </a>. +settings.add_webhook_desc=Gitea надішле запити <code>POST</code> із зазначеним типом зміÑту на цільову URL-адреÑу. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в <a target="_blank" rel="noopener noreferrer" href="%s">інÑтрукції по викориÑтанню веб-хуків</a>. settings.payload_url=Цільова URL-адреÑа settings.http_method=Метод HTTP settings.content_type=Тип зміÑту settings.secret=Секрет settings.slack_username=Ім'Ñ ÐºÑ€Ð¸Ñтувача -settings.slack_icon_url=URL іконки +settings.slack_icon_url=URL піктограми settings.slack_color=Колір settings.discord_username=Ім'Ñ ÐºÑ€Ð¸Ñтувача -settings.discord_icon_url=URL іконки +settings.discord_icon_url=URL піктограми settings.event_desc=Тригер: -settings.event_push_only=Push події settings.event_send_everything=Ð’ÑÑ– події settings.event_choose=ВлаÑні події… settings.event_header_repository=Події репозиторію @@ -1957,40 +1956,35 @@ settings.event_create_desc=Гілку або тег Ñтворено. settings.event_delete=Видалити settings.event_delete_desc=Гілку або мітку було видалено. settings.event_fork=Форк -settings.event_fork_desc=Репозиторій було форкнуто. settings.event_wiki=Вікі settings.event_statuses=СтатуÑи settings.event_statuses_desc=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ оновлено з API. settings.event_release=Реліз -settings.event_release_desc=Реліз опублікований, оновлений або видалений з репозиторіÑ. +settings.event_release_desc=Реліз опубліковано, оновлено або видалено зі Ñховища. settings.event_push=Push -settings.event_push_desc=Git push до репозиторію. settings.event_repository=Репозиторій settings.event_repository_desc=Репозиторій Ñтворений або видалено. settings.event_header_issue=Події задачі settings.event_issues=Задачі -settings.event_issues_desc=Задача відкрита, закрита, повторно відкрита або відредагована. -settings.event_issue_assign=Задача прив'Ñзана +settings.event_issues_desc=Задачу відкрито, закрито, повторно відкрито або відредаговано. +settings.event_issue_assign=Задачу призначено settings.event_issue_assign_desc=Задачу призначено або ÑкаÑовано. -settings.event_issue_label=Задача з міткою settings.event_issue_label_desc=Мітки задачі оновлено або видалено. -settings.event_issue_milestone=Задача з етапом -settings.event_issue_milestone_desc=Задача призначена на етап або видалена з етапу. settings.event_issue_comment=Коментар задачі settings.event_issue_comment_desc=Коментар задачі Ñтворено, видалено чи відредаговано. settings.event_header_pull_request=Події запиту Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -settings.event_pull_request=Запити до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -settings.event_pull_request_desc=Запит до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ð¾, закрито, перевідкрито або відредаговано. +settings.event_pull_request=Запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +settings.event_pull_request_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ð¾, закрито, повторно відкрито або відредаговано. settings.event_pull_request_assign=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¾ -settings.event_pull_request_assign_desc=Запит про Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¾ або ÑкаÑовано. +settings.event_pull_request_assign_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¾ або ÑкаÑовано. settings.event_pull_request_label=Запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð° мітка settings.event_pull_request_label_desc=Мітка запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð° або очищена. -settings.event_pull_request_milestone=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ð¹ на етап -settings.event_pull_request_milestone_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ð¹ на етап або видалений з етапу. -settings.event_pull_request_comment=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¾ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð¾Ð²Ð°Ð½Ð¸Ð¹ +settings.event_pull_request_milestone=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð¾Ð´Ð°Ð½Ð¾ до етапу +settings.event_pull_request_milestone_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð¾Ð´Ð°Ð½Ð¾ до етапу або видалено з етапу. +settings.event_pull_request_comment=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¾ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð¾Ð²Ð°Ð½Ð¾ settings.event_pull_request_comment_desc=Коментар запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñтворено, відредаговано чи видалено. settings.event_pull_request_review=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ€ÐµÑ†ÐµÐ½Ð·Ð¾Ð²Ð°Ð½Ð¾ -settings.event_pull_request_review_desc=Коментар запиту до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð¸Ð¹, відхилений або рецензований. +settings.event_pull_request_review_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð¾, відхилено або прокоментовано. settings.event_pull_request_sync=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ ÑинхронізуєтьÑÑ settings.event_pull_request_sync_desc=Запит до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñинхронізовано. settings.branch_filter=Фільтр гілок @@ -2020,28 +2014,24 @@ settings.web_hook_name_wechatwork=WeCom (Wechat Work) settings.web_hook_name_packagist=Packagist settings.packagist_username=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Packagist settings.packagist_api_token=Токен API -settings.deploy_keys=Ключі Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.add_deploy_key=Додати ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.deploy_key_desc=Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупні тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ. Це не те ж Ñаме що Ñ– SSH-ключі аккаунта. +settings.deploy_keys=Ключі Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ +settings.add_deploy_key=Додати ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ +settings.deploy_key_desc=Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð¼Ð°ÑŽÑ‚ÑŒ доÑтуп до Ñховища лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ. settings.is_writable=Увімкнути доÑтуп Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñу -settings.is_writable_info=Чи може цей ключ бути викориÑтаний Ð´Ð»Ñ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ <strong>push</strong> в репозиторій? Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð·Ð°Ð²Ð¶Ð´Ð¸ мають доÑтуп на pull. -settings.no_deploy_keys=Ви не додавали ключі розгортаннÑ. +settings.no_deploy_keys=Ключів Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñ‰Ðµ немає. settings.title=Заголовок settings.deploy_key_content=ЗміÑÑ‚ -settings.key_been_used=ЗміÑÑ‚ ключа Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð²Ð¶Ðµ викориÑтовуєтьÑÑ. -settings.key_name_used=Ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð· таким заголовком вже Ñ–Ñнує. -settings.deploy_key_deletion=Видалити ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.deploy_key_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð° розгортки унеможливить доÑтуп до Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ð· його допомогою. Ви впевнені? +settings.key_been_used=Ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð· ідентичним вміÑтом вже викориÑтовуєтьÑÑ. +settings.key_name_used=Ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð· такою ж назвою вже Ñ–Ñнує. +settings.deploy_key_deletion=Видалити ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ +settings.deploy_key_deletion_desc=Ð’Ð¸Ð»ÑƒÑ‡ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð° Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¸Ð·Ð²ÐµÐ´Ðµ до Ð²Ñ–Ð´ÐºÐ»Ð¸ÐºÐ°Ð½Ð½Ñ Ð¹Ð¾Ð³Ð¾ доÑтупу до цього Ñховища. Продовжити? settings.deploy_key_deletion_success=Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð±ÑƒÐ»Ð¾ видалено. settings.branches=Гілки settings.protected_branch=ЗахиÑÑ‚ гілки settings.protected_branch.save_rule=Зберегти правило settings.protected_branch.delete_rule=Видалити правило -settings.protected_branch_can_push=Дозволити push? -settings.protected_branch_can_push_yes=Ви можете виконувати push settings.branch_protection=ЗахиÑÑ‚ гілки '<b>%s</b>' -settings.protect_this_branch=ЗахиÑтити цю гілку -settings.protect_this_branch_desc=Запобігає видаленню гілки та обмежує Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð² ній push та злиттÑ. +settings.protect_this_branch=Увімкнути захиÑÑ‚ гілок settings.protect_disable_push=Заборонити Push settings.protect_disable_push_desc=Ð”Ð»Ñ Ñ†Ñ–Ñ”Ñ— гілки буде заборонено Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ push. settings.protect_enable_push=Дозволити Push @@ -2060,35 +2050,35 @@ settings.require_signed_commits_desc=ВідхилÑти push до цієї гіРsettings.add_protected_branch=Увімкнути захиÑÑ‚ settings.delete_protected_branch=Вимкнути захиÑÑ‚ settings.protected_branch_deletion_desc=Будь-Ñкий кориÑтувач з дозволами на Ð·Ð°Ð¿Ð¸Ñ Ð·Ð¼Ð¾Ð¶Ðµ виконувати push в цю гілку. Ви впевнені? -settings.block_rejected_reviews=Блокувати Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸ відкидаючих рецензіÑÑ… -settings.block_rejected_reviews_desc=Ð—Ð»Ð¸Ñ‚Ñ‚Ñ Ð±ÑƒÐ´Ðµ недоÑтупним, Ñкщо Ñ” запит змін від офіційних рецензентів, навіть за наÑвноÑті доÑтатньої кількоÑті Ñхвалень. -settings.block_on_official_review_requests=Блокувати Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸ запиті на офіціальний розглÑд +settings.block_rejected_reviews=Блокувати об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñкщо рецензії відхилено +settings.block_rejected_reviews_desc=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ неможливим, Ñкщо офіційні рецензенти вимагають внеÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½, навіть Ñкщо Ñ” доÑÑ‚Ð°Ñ‚Ð½Ñ ÐºÑ–Ð»ÑŒÐºÑ–Ñть Ñхвалень. +settings.block_on_official_review_requests=Блокувати об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ð° офіційними запитами на Ñ€ÐµÑ†ÐµÐ½Ð·ÑƒÐ²Ð°Ð½Ð½Ñ settings.block_on_official_review_requests_desc=ÐžÐ±â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ðµ, коли воно має офіційні запити на розглÑд, навіть Ñкщо доÑтатньо Ñхвалень. -settings.block_outdated_branch=Блокувати злиттÑ, Ñкщо запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ñтарів -settings.block_outdated_branch_desc=Ð—Ð»Ð¸Ñ‚Ñ‚Ñ Ð±ÑƒÐ´Ðµ неможливим, коли головна гілка позаду оÑновної. -settings.default_branch_desc=Головна гілка Ñ” 'базовою' Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ репозиторіÑ, на Ñку за замовчуваннÑм ÑпрÑмовані вÑÑ– запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– Ñка Ñ” обличчÑм вашого репозиторіÑ. Перше, що побачить відвідувач - це зміÑÑ‚ головної гілки. Виберіть Ñ—Ñ— з уже Ñ–Ñнуючих: +settings.block_outdated_branch=Блокувати об'єднаннÑ, Ñкщо запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ñтарів +settings.block_outdated_branch_desc=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ неможливим, Ñкщо головна гілка позаду оÑновної. +settings.default_branch_desc=Обрати типову гілку Ñховища Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– комітів: settings.choose_branch=Оберіть гілку… settings.no_protected_branch=Ðемає захищених гілок. settings.edit_protected_branch=Редагувати settings.protected_branch_required_approvals_min=ЧиÑло необхідних Ñхвалень не може бути від'ємним. settings.tags=Мітки settings.tags.protection=ЗахиÑÑ‚ мітки -settings.tags.protection.pattern=Шаблон тега +settings.tags.protection.pattern=Шаблон мітки settings.tags.protection.allowed=Дозволено settings.tags.protection.allowed.users=Дозволені кориÑтувачі settings.tags.protection.allowed.teams=Дозволені команди settings.tags.protection.allowed.noone=Ðіхто -settings.tags.protection.create=ЗахиÑтна мітка +settings.tags.protection.create=ЗахиÑтити мітку settings.tags.protection.none=Там не немає захищених міток. settings.bot_token=Токен Ð´Ð»Ñ Ð±Ð¾Ñ‚Ð° -settings.chat_id=Чат ID +settings.chat_id=ID чату settings.matrix.homeserver_url=URL домашньої Ñторінки -settings.matrix.room_id=Ðомер кімнати +settings.matrix.room_id=ID кімнати settings.matrix.message_type=Тип Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ settings.archive.header=Відправити репозиторій в архів -settings.archive.success=Репозиторію уÑпішно приÑвоєно ÑÑ‚Ð°Ñ‚ÑƒÑ Ð°Ñ€Ñ…Ñ–Ð²Ð½Ð¾Ð³Ð¾. -settings.archive.error=СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при Ñпробі архівувати репозиторій. Докладнішу інформацію див. у журналі. -settings.archive.error_ismirror=Ðеможливо архівувати дзеркальний репозиротрій. +settings.archive.success=Сховище уÑпішно заархівовано. +settings.archive.error=СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при Ñпробі архівувати репозиторій. Докладнішу інформацію дивітьÑÑ Ñƒ журналі. +settings.archive.error_ismirror=Ðеможливо архівувати дзеркальне Ñховище. settings.archive.branchsettings_unavailable=Параметри гілки не доÑтупні, Ñкщо репозиторій архівний. settings.archive.tagsettings_unavailable=Параметри міток недоÑтупні, Ñкщо репозиторій архівний. settings.update_avatar_success=Ðватар репозиторію оновлений. @@ -2102,9 +2092,9 @@ settings.lfs_delete=Видалити файл LFS з OID %s settings.lfs_delete_warning=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñƒ LFS може Ñпричинити помилки "Об'єкт не Ñ–Ñнує" під Ñ‡Ð°Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸. Ви впевнені? settings.lfs_findpointerfiles=Знайти файли-поÑÐ¸Ð»Ð°Ð½Ð½Ñ settings.lfs_locks=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ -settings.lfs_invalid_locking_path=ÐеприпуÑтимий шлÑÑ…: %s +settings.lfs_invalid_locking_path=ÐедійÑний шлÑÑ…: %s settings.lfs_invalid_lock_directory=Ðе можливо заблокувати каталог: %s -settings.lfs_lock_already_exists=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¶Ðµ викориÑтовуєтьÑÑ: %s +settings.lfs_lock_already_exists=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¶Ðµ Ñ–Ñнує: %s settings.lfs_lock=Блокувати settings.lfs_lock_path=ШлÑÑ… до файлу Ð´Ð»Ñ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ... settings.lfs_locks_no_locks=ВідÑутнє Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ @@ -2144,7 +2134,7 @@ diff.stats_desc_file=%d змін: %d доповнень та %d видалень diff.bin=BIN diff.bin_not_shown=Бінарний файл не відображаєтьÑÑ. diff.view_file=ПереглÑнути файл -diff.file_before=Перед +diff.file_before=До diff.file_after=ПіÑÐ»Ñ diff.file_image_width=Ширина diff.file_image_height=ВиÑота @@ -2163,15 +2153,15 @@ diff.comment.start_review=Розпочати рецензію diff.comment.reply=Відповідь diff.review=Ð ÐµÑ†ÐµÐ½Ð·Ñ–Ñ diff.review.header=ÐадіÑлати рецензію -diff.review.placeholder=Рецензійований коментарій +diff.review.placeholder=Ð ÐµÑ†ÐµÐ½Ð·Ñ–Ñ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ diff.review.comment=Коментар diff.review.approve=Затвердити diff.review.reject=Запит змін diff.committed_by=зафікÑовано diff.protected=Захищений -diff.image.side_by_side=Пліч-о-пліч -diff.image.swipe=Свайп -diff.image.overlay=Оверлей +diff.image.side_by_side=Поруч +diff.image.swipe=ПровеÑти пальцем +diff.image.overlay=ÐаклаÑти diff.show_file_tree=Показати дерево файлів diff.hide_file_tree=Сховати дерево файлів diff.submodule_added=Підмодуль %[1]s додано в %[2]s @@ -2181,7 +2171,7 @@ diff.submodule_updated=Підмодуль %[1]s оновлено: %[2]s releases.desc=ВідÑлідковувати верÑÑ–Ñ— проєкту Ñ– завантаженнÑ. release.releases=Релізи release.detail=Деталі релізу -release.tags=Теги +release.tags=Мітки release.new_release=Ðовий реліз release.draft=Чернетка release.prerelease=Пре-реліз @@ -2191,37 +2181,37 @@ release.edit=редагувати release.ahead.commits=<strong>%d</strong> коміт(ів) release.ahead.target=до %s з моменту цього випуÑку release.source_code=Код -release.new_subheader=ÐŸÑƒÐ±Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ Ñ€ÐµÐ»Ñ–Ð·Ñ–Ð² допоможе вам організувати верÑÑ–ÑŽ проєкту. -release.edit_subheader=ÐŸÑƒÐ±Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ Ñ€ÐµÐ»Ñ–Ð·Ñ–Ð² допоможе вам організувати верÑÑ–ÑŽ проєкту. -release.tag_name=Ðазва тегу +release.new_subheader=Релізи впорÑдковують верÑÑ–Ñ— проєкту. +release.edit_subheader=Релізи впорÑдковують верÑÑ–Ñ— проєкту. +release.tag_name=Ðазва мітки release.target=Ціль -release.tag_helper=Виберіть Ñ–Ñнуючий тег або Ñтворіть новий. +release.tag_helper=Вибрати Ñ–Ñнуючу мітку або Ñтворити нову. release.title=Ðазва релізу release.title_empty=Заголовок не може бути порожнім. release.message=Опишіть цей реліз release.prerelease_desc=Позначити Ñк пре-реліз -release.prerelease_helper=Позначте цей випуÑк непридатним Ð´Ð»Ñ ÐŸÐ ÐžÐ” викориÑтаннÑ. +release.prerelease_helper=Позначите випуÑк Ñк непридатний Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð´ÑƒÐºÑ‚Ð¸Ð²Ð½Ð¾Ð³Ð¾ викориÑтаннÑ. release.cancel=Відмінити release.publish=Опублікувати реліз release.save_draft=Зберегти чернетку release.edit_release=Оновити реліз release.delete_release=Видалити реліз -release.delete_tag=Видалити тег +release.delete_tag=Видалити мітку release.deletion=Видалити реліз -release.deletion_success=Реліз, було видалено. +release.deletion_success=Реліз видалено. release.deletion_tag_desc=Буде видалено цей тег із репозиторію. ВміÑÑ‚ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ñ‚Ð° Ñ–ÑÑ‚Ð¾Ñ€Ñ–Ñ Ð·Ð°Ð»Ð¸ÑˆÐ°Ñ‚ÑŒÑÑ Ð½ÐµÐ·Ð¼Ñ–Ð½Ð½Ð¸Ð¼Ð¸. Продовжити? release.deletion_tag_success=Мітка видалена. -release.tag_name_already_exist=Реліз з цим ім'Ñм мітки вже Ñ–Ñнує. -release.tag_name_invalid=ÐеприпуÑтиме ім'Ñ Ñ‚ÐµÐ³Ð°. -release.tag_name_protected=Ім'Ñ Ñ‚ÐµÐ³Ð° захищене. -release.tag_already_exist=Цей тег вже викориÑтовуєтьÑÑ. -release.downloads=Завантажити +release.tag_name_already_exist=Реліз з такою ж міткою вже Ñ–Ñнує. +release.tag_name_invalid=Ðазва мітки недійÑна. +release.tag_name_protected=Ðазва мітки захищена. +release.tag_already_exist=Ðазва мітки вже Ñ–Ñнує. +release.downloads=Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ release.download_count=ЗавантаженнÑ: %s -release.add_tag_msg=ВикориÑтовуйте заголовок Ñ– зміÑÑ‚ релізу Ñк Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñк тег повідомленнÑ. +release.add_tag_msg=ВикориÑтовуйте заголовок Ñ– зміÑÑ‚ релізу Ñк Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–Ñ‚ÐºÐ¸. release.add_tag=Створити тільки мітку release.releases_for=Релізи Ð´Ð»Ñ %s -branch.name=Ім'Ñ Ð³Ñ–Ð»ÐºÐ¸ +branch.name=Ðазва гілки branch.already_exists=Гілка з назвою "%s" вже Ñ–Ñнує. branch.delete_head=Видалити branch.delete=`Видалити гілку "%s"` @@ -2253,7 +2243,7 @@ branch.rename_default_or_protected_branch_error=Лише адмініÑтратРtag.create_tag=Створити тег %s -topic.manage_topics=Керувати тематичними мітками +topic.manage_topics=Керувати темами topic.done=Готово topic.count_prompt=Ви не можете вибрати більше ніж 25 тем topic.format_prompt=Теми мають починатиÑÑ Ð· літери або цифри, можуть міÑтити дефіÑи ('-') Ñ– крапки ('.'), мати довжину до 35 Ñимволів. Літери повинні бути малими. @@ -2289,7 +2279,7 @@ team_name=Ðазва команди team_desc=ÐžÐ¿Ð¸Ñ team_name_helper=Ðазва команди має бути проÑтою та зрозумілою. team_desc_helper=Опишіть мету або роль команди. -team_access_desc=ДоÑтуп до Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ +team_access_desc=ДоÑтуп до Ñховища team_permission_desc=Права доÑтупу team_unit_desc=Дозволити доÑтуп до розділів репозиторію team_unit_disabled=(Вимкнено) @@ -2306,28 +2296,28 @@ settings.location=Ð Ð¾Ð·Ñ‚Ð°ÑˆÑƒÐ²Ð°Ð½Ð½Ñ settings.permission=Дозволи settings.repoadminchangeteam=ÐдмініÑтратор репозитарію може додавати та видалÑти доÑтуп Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´ settings.visibility=ВидиміÑть -settings.visibility.public=Публічний -settings.visibility.limited_shortname=Обмежений -settings.visibility.private=Приватний (Видимий лише членам організації) +settings.visibility.public=Публічна +settings.visibility.limited_shortname=Обмежена +settings.visibility.private=Приватна (Видима лише членам організації) settings.visibility.private_shortname=Приватний settings.update_settings=Оновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ settings.update_setting_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— оновлені. -settings.change_orgname_redirect_prompt=Старе ім'Ñ Ð±ÑƒÐ´Ðµ перенаправлено до тих пір, поки воно не буде заброньовано. +settings.change_orgname_redirect_prompt=Стара назва буде перенаправлÑтиÑÑ Ð´Ð¾ тих пір, поки не буде заброньована. settings.update_avatar_success=Ðватар організації оновлений. settings.delete=Видалити організацію settings.delete_account=Видалити цю організацію -settings.delete_prompt=ÐžÑ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ оÑтаточно видалена. Це <strong>ÐЕ МОЖЛИВО</strong> відмінити! -settings.confirm_delete_account=Підтвердіть Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ +settings.delete_prompt=Організацію буде оÑтаточно видалено. Це <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати! +settings.confirm_delete_account=Підтвердити Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ settings.delete_org_title=Видалити організацію settings.delete_org_desc=Ð¦Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ безповоротно видалена. Продовжити? -settings.hooks_desc=Додайте webhooks, Ñкий буде викликатиÑÑ Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong> Ñкими володіє Ñ†Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ. +settings.hooks_desc=Додайте веб-хуки, Ñкі Ñпрацьовуватимуть Ð´Ð»Ñ <strong>вÑÑ–Ñ… Ñховищ</strong> у цій організації. -settings.labels_desc=Додати мітки, Ñкі можуть бути викориÑтані Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ñ‡ Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong> в цій організації. +settings.labels_desc=Додайте мітки, Ñкі можна викориÑтовувати у задачах Ð´Ð»Ñ <strong>уÑÑ–Ñ… Ñховищ</strong> у цій організації. members.membership_visibility=ВидиміÑть учаÑника: members.public=Показувати -members.public_helper=зробити прихованим +members.public_helper=приховати members.private=Прихований members.private_helper=зробити видимим members.member_role=Роль учаÑника: @@ -2355,7 +2345,7 @@ teams.admin_access=ДоÑтуп адмініÑтратора teams.admin_access_helper=УчаÑники можуть виконувати pull, push в репозиторії команд Ñ– додавати Ñпівавторів в команду. teams.no_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° не має опиÑу teams.settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ -teams.owners_permission_desc=ВлаÑник має повний доÑтуп до <strong>уÑÑ–Ñ… репозиторіїв</strong> та має <strong>права адмініÑтратора</strong> організації. +teams.owners_permission_desc=ВлаÑники мають повний доÑтуп до <strong>уÑÑ–Ñ… репозиторіїв</strong> та <strong>права адмініÑтратора</strong> організації. teams.members=УчаÑники команди teams.update_settings=Оновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ teams.delete_team=Видалити команду @@ -2364,7 +2354,7 @@ teams.invite_team_member=ЗапроÑити до %s teams.delete_team_title=Видалити команду teams.delete_team_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¸ ÑкаÑовує доÑтуп до Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ð´Ð»Ñ Ñ—Ñ— учаÑників. Продовжити? teams.delete_team_success=Команду було видалено. -teams.read_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° має доÑтуп Ð´Ð»Ñ <strong>читаннÑ</strong>: учаÑники можуть переглÑдати та клонувати репозиторії. +teams.read_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° має доÑтуп на <strong>читаннÑ</strong>: учаÑники можуть переглÑдати та клонувати репозиторії. teams.write_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає доÑтуп на <strong>запиÑ</strong>: учаÑники можуть отримувати й виконувати push команди до репозитрію. teams.admin_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає <strong>адмініÑтраторÑький</strong> доÑтуп: учаÑники можуть читати, виконувати push команди та додавати Ñпівробітників до репозиторію. teams.create_repo_permission_desc=Крім того, Ñ†Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>Створити репозиторій</strong>: учаÑники можуть Ñтворювати нові репозиторії в організації. @@ -2409,7 +2399,7 @@ repositories=Репозиторії hooks=Веб-хуки integrations=Інтеграції authentication=Джерела автентифікації -emails=Електронні адреÑи КориÑтувача +emails=Електронна пошта кориÑтувача config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ config_summary=ПідÑумок config_settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ @@ -2437,12 +2427,12 @@ dashboard.cron.process=Cron: %[1]s dashboard.cron.error=Помилка в Cron: %s: %[3]s dashboard.cron.finished=Cron: %[1]s завершено dashboard.delete_inactive_accounts=Видалити вÑÑ– неактивовані облікові запиÑи -dashboard.delete_inactive_accounts.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ– неактивованих облікових запиÑів. +dashboard.delete_inactive_accounts.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… неактивованих облікових запиÑів. dashboard.delete_repo_archives=Видалити вÑÑ– архіви репозиторіїв (ZIP, TAR.GZ, Ñ– Ñ‚. д..) dashboard.delete_repo_archives.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… архівів репозиторіїв. -dashboard.delete_missing_repos=Видалити вÑÑ– запиÑи про репозиторії з відÑутніми файлами Git +dashboard.delete_missing_repos=Видаліть уÑÑ– Ñховища, в Ñких відÑутні файли Git dashboard.delete_missing_repos.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… репозиторіїв, в Ñких відÑутні файли Git. -dashboard.delete_generated_repository_avatars=Видалити репозиторій з згенерованими аватарами +dashboard.delete_generated_repository_avatars=Видалити згенеровані аватарки Ñховища dashboard.update_mirrors=Оновити дзеркала dashboard.repo_health_check=Перевірка Ñтану вÑÑ–Ñ… репозиторіїв dashboard.check_repo_stats=Перевірити ÑтатиÑтику вÑÑ–Ñ… репозиторіїв @@ -2452,11 +2442,11 @@ dashboard.update_migration_poster_id=Оновити мігровані ID авт dashboard.git_gc_repos=Виконати очиÑтку ÑÐ¼Ñ–Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ð²ÑÑ–Ñ… репозиторіїв dashboard.resync_all_sshkeys=Оновити файл '.ssh/authorized_keys' з SSH ключами Gitea. dashboard.resync_all_sshprincipals=Оновіть файл '.ssh/authorized_princÑ‚ipals' з SSH даними кориÑтувача Gitea. -dashboard.resync_all_hooks=ПереÑинхронізувати перед-прийнÑтні, оновлюючі та поÑÑ‚-прийнÑтні хуки в уÑÑ–Ñ… репозиторіÑÑ…. -dashboard.reinit_missing_repos=Переініціалізувати уÑÑ– репозитрії git-файли Ñких втрачено +dashboard.resync_all_hooks=Заново Ñинхронізувати хуки попереднього отриманнÑ, Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‚Ð° поÑÑ‚-Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð²ÑÑ–Ñ… Ñховищ. +dashboard.reinit_missing_repos=Заново ініціалізувати вÑÑ– відÑутні Ñховища Git'а, Ð´Ð»Ñ Ñких Ñ–Ñнують запиÑи dashboard.sync_external_users=Синхронізувати дані зовнішніх кориÑтувачів -dashboard.cleanup_hook_task_table=ОчиÑтити hook_task таблицю -dashboard.server_uptime=Uptime Ñерверу +dashboard.cleanup_hook_task_table=ОчиÑтити таблицю hook_task +dashboard.server_uptime=Ð§Ð°Ñ Ñ€Ð¾Ð±Ð¾Ñ‚Ð¸ Ñервера dashboard.current_goroutine=Поточна кількіÑть Goroutines dashboard.current_memory_usage=Поточне викориÑÑ‚Ð°Ð½Ð½Ñ Ð¿Ð°Ð¼'Ñті dashboard.total_memory_allocated=Виділено пам'Ñті загалом @@ -2526,8 +2516,8 @@ users.allow_create_organization=Може Ñтворювати організац users.update_profile=Оновити обліковий Ð·Ð°Ð¿Ð¸Ñ users.delete_account=Видалити цей обліковий Ð·Ð°Ð¿Ð¸Ñ users.cannot_delete_self=Ви не можете видалити Ñебе -users.still_own_repo=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ñе ще володіє одним або кількома репозиторіÑми, Ñпочатку вам потрібно видалити або передати Ñ—Ñ…. -users.still_has_org=Цей обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ñе ще Ñ” учаÑником однієї або декількох організацій. Ð”Ð»Ñ Ð¿Ñ€Ð¾Ð´Ð¾Ð²Ð¶ÐµÐ½Ð½Ñ, покиньте або видаліть організації. +users.still_own_repo=Цей кориÑтувач вÑе ще володіє одним або кількома Ñховищами. Спочатку видаліть або передайте ці Ñховища. +users.still_has_org=Цей кориÑтувач Ñ” членом організації. Спочатку видаліть кориÑтувача з уÑÑ–Ñ… організацій. users.purge=Видалити кориÑтувача users.deletion_success=Обліковий Ð·Ð°Ð¿Ð¸Ñ ÐºÐ¾Ñ€Ð¸Ñтувача було видалено. users.reset_2fa=Скинути 2FA @@ -2558,8 +2548,8 @@ emails.duplicate_active=Ð¦Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð° адреÑа вже актив emails.change_email_header=Редагувати влаÑтивоÑті електронної пошти emails.change_email_text=Ви впевнені, що хочете оновити адреÑу електронної пошти? emails.delete_desc=Ви впевнені, що бажаєте видалити цю електронну адреÑу? -emails.deletion_success=Електронну адреÑу видалено. -emails.delete_primary_email_error=Ви не можете видалити оÑновну електронну адреÑу. +emails.deletion_success=ÐдреÑу електронної пошти видалено. +emails.delete_primary_email_error=Ви не можете видалити оÑновну адреÑу електронної пошти. orgs.org_manage_panel=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñми orgs.name=Ðазва @@ -2600,7 +2590,7 @@ systemhooks.update_webhook=Оновити ÑиÑтемний вебхук auths.auth_manage_panel=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¶ÐµÑ€ÐµÐ»Ð¾Ð¼ автентифікації auths.new=Додати джерело автентифікації -auths.name=Ім'Ñ +auths.name=Ðазва auths.type=Тип auths.enabled=Увімкнено auths.syncenabled=Увімкнути Ñинхронізацію кориÑтувача @@ -2683,19 +2673,19 @@ auths.tip.discord=ЗареєÑтрувати новий додаток на %s auths.tip.gitea=ЗареєÑтруйте новий додаток OAuth2. ПоÑібник можна знайти за поÑиланнÑм %s auths.tip.mastodon=Введіть URL Ñпеціального екземплÑра Ð´Ð»Ñ ÐµÐºÐ·ÐµÐ¼Ð¿Ð»Ñра mastodon, Ñкий ви хочете автентифікувати за допомогою (або викориÑтовувати за замовчуваннÑм) auths.edit=Редагувати джерело автентифікації -auths.activated=Ð¦Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ Ð°ÐºÑ‚Ð¸Ð²Ð¾Ð²Ð°Ð½Ð° +auths.activated=Це джерело автентифікації активовано auths.new_success=Ðвтентифікацію "%s" додано. auths.update_success=Параметри аутентифікації оновлені. auths.update=Оновити джерело автентифікації auths.delete=Видалити джерело автентифікації auths.delete_auth_title=Видалити джерело автентифікації -auths.delete_auth_desc=Це джерело аутентифікації буде видалене, ви впевнені, що ви хочете продовжити? -auths.still_in_used=Ð¦Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ° ÑправжноÑті доÑÑ– викориÑтовуєтьÑÑ Ð´ÐµÑкими кориÑтувачами. Видаліть або змініть Ð´Ð»Ñ Ñ†Ð¸Ñ… кориÑтувачів тип входу в ÑиÑтему. -auths.deletion_success=Канал аутентифікації уÑпішно знищений. +auths.delete_auth_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð´Ð¶ÐµÑ€ÐµÐ»Ð° автентифікації заборонÑÑ” кориÑтувачам викориÑтовувати його Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ. Продовжити? +auths.still_in_used=Джерело автентифікації вÑе ще викориÑтовуєтьÑÑ. Спочатку перетворіть або видаліть кориÑтувачів, Ñкі викориÑтовують це джерело автентифікації. +auths.deletion_success=Джерело автентифікації видалено. auths.login_source_exist=Джерело автентифікації "%s" вже Ñ–Ñнує. -auths.login_source_of_type_exist=Джерело автентифікації такого типу вже наÑвне. +auths.login_source_of_type_exist=Джерело автентифікації такого типу вже Ñ–Ñнує. -config.server_config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ñервера +config.server_config=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñервера config.app_name=Ðазва Ñайту config.app_ver=ВерÑÑ–Ñ Gitea config.app_url=Базова URL-адреÑа Gitea @@ -2703,14 +2693,14 @@ config.custom_conf=ШлÑÑ… до файлу конфігурації config.custom_file_root_path=ШлÑÑ… до файлу кориÑтувача config.domain=Домен Ñервера config.offline_mode=Локальний режим -config.disable_router_log=Вимкнути Ð»Ð¾Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€Ð¾ÑƒÑ‚ÐµÑ€Ñƒ +config.disable_router_log=Вимкнути журнал маршрутизатора config.run_user=ЗапуÑк від імені КориÑтувача config.run_mode=Режим Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ config.git_version=ВерÑÑ–Ñ Git config.app_data_path=ШлÑÑ… до даних додатка config.repo_root_path=Кореневий шлÑÑ… Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ -config.lfs_root_path=Кореневої шлÑÑ… LFS -config.log_file_root_path=ШлÑÑ… до лог файлу +config.lfs_root_path=Кореневий шлÑÑ… LFS +config.log_file_root_path=ШлÑÑ… до журналу config.script_type=Тип Ñкрипта config.reverse_auth_user=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ñ–Ñ— на reverse proxy diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 989802b9db..b88377cce8 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -463,7 +463,7 @@ oauth_signin_submit=ç»‘å®šè´¦å· oauth.signin.error.general=å¤„ç†æŽˆæƒè¯·æ±‚时出错:%s。如果æ¤é”™è¯¯ä»ç„¶å˜åœ¨ï¼Œè¯·ä¸Žç«™ç‚¹ç®¡ç†å‘˜è”系。 oauth.signin.error.access_denied=授æƒè¯·æ±‚被拒ç»ã€‚ oauth.signin.error.temporarily_unavailable=授æƒå¤±è´¥ï¼Œå› ä¸ºè®¤è¯æœåŠ¡å™¨æš‚æ—¶ä¸å¯ç”¨ã€‚请ç¨åŽå†è¯•。 -oauth_callback_unable_auto_reg=自动注册已å¯ç”¨ï¼Œä½†OAuth2 æä¾›å•† %[1]s è¿”å›žç¼ºå¤±çš„å—æ®µï¼š%[2]sï¼Œæ— æ³•è‡ªåŠ¨åˆ›å»ºå¸æˆ·ï¼Œè¯·åˆ›å»ºæˆ–é“¾æŽ¥åˆ°ä¸€ä¸ªå¸æˆ·ï¼Œæˆ–è”系站点管ç†å‘˜ã€‚ +oauth_callback_unable_auto_reg=自动注册已å¯ç”¨ï¼Œä½† OAuth2 æä¾›å•† %[1]s è¿”å›žç¼ºå¤±çš„å—æ®µï¼š%[2]sï¼Œæ— æ³•è‡ªåŠ¨åˆ›å»ºå¸æˆ·ï¼Œè¯·åˆ›å»ºæˆ–é“¾æŽ¥åˆ°ä¸€ä¸ªå¸æˆ·ï¼Œæˆ–è”系站点管ç†å‘˜ã€‚ openid_connect_submit=连接 openid_connect_title=è¿žæŽ¥åˆ°çŽ°æœ‰çš„å¸æˆ· openid_connect_desc=所选的 OpenID URI 未知。在这里关è”ä¸€ä¸ªæ–°å¸æˆ·ã€‚ @@ -1161,8 +1161,8 @@ template.issue_labels=工啿 ‡ç¾ template.one_item=必须至少选择一个模æ¿é¡¹ template.invalid=必须选择一个模æ¿ä»“库 -archive.title=该仓库已被归档。您å¯ä»¥æŸ¥çœ‹æ–‡ä»¶å’Œå…‹éš†å®ƒï¼Œä½†ä¸èƒ½æŽ¨é€ã€æ‰“开工啿ˆ–åˆå¹¶è¯·æ±‚。 -archive.title_date=该仓库已于 %s 归档。您å¯ä»¥æŸ¥çœ‹æ–‡ä»¶æˆ–克隆它,但ä¸èƒ½æŽ¨é€ã€æ‰“开工啿ˆ–åˆå¹¶è¯·æ±‚。 +archive.title=该仓库已被归档。您å¯ä»¥æŸ¥çœ‹æ–‡ä»¶å’Œå…‹éš†å®ƒï¼Œä½†ä¸èƒ½æŽ¨é€ã€åˆ›å»ºå·¥å•或åˆå¹¶è¯·æ±‚。 +archive.title_date=该仓库已于 %s 归档。您å¯ä»¥æŸ¥çœ‹æ–‡ä»¶æˆ–克隆它,但ä¸èƒ½æŽ¨é€ã€åˆ›å»ºå·¥å•或åˆå¹¶è¯·æ±‚。 archive.issue.nocomment=æ¤ä»“åº“å·²å˜æ¡£ï¼Œæ‚¨ä¸èƒ½åœ¨æ¤å·¥å•æ·»åŠ è¯„è®ºã€‚ archive.pull.nocomment=æ¤ä»“åº“å·²å˜æ¡£ï¼Œæ‚¨ä¸èƒ½åœ¨æ¤åˆå¹¶è¯·æ±‚æ·»åŠ è¯„è®ºã€‚ @@ -1506,7 +1506,7 @@ issues.choose.blank_about=从默认模æ¿åˆ›å»ºä¸€ä¸ªå·¥å•。 issues.choose.ignore_invalid_templates=å·²å¿½ç•¥æ— æ•ˆæ¨¡æ¿ issues.choose.invalid_templates=å‘现了 %v ä¸ªæ— æ•ˆæ¨¡æ¿ issues.choose.invalid_config=问题é…置包å«é”™è¯¯ï¼š -issues.no_ref=分支/æ ‡è®°æœªæŒ‡å®š +issues.no_ref=分支/Gitæ ‡ç¾æœªæŒ‡å®š issues.create=åˆ›å»ºå·¥å• issues.new_label=åˆ›å»ºæ ‡ç¾ issues.new_label_placeholder=æ ‡ç¾åç§° @@ -2375,7 +2375,7 @@ settings.event_issues=å·¥å• settings.event_issues_desc=å·¥å•已打开ã€å·²å…³é—ã€å·²é‡æ–°æ‰“开或已编辑。 settings.event_issue_assign=å·¥å•已指派 settings.event_issue_assign_desc=å·¥å•å·²æŒ‡æ´¾æˆ–å–æ¶ˆæŒ‡æ´¾ã€‚ -settings.event_issue_label=å·²æ ‡è®°å·¥å• +settings.event_issue_label=å·¥å•å¢žåˆ æ ‡ç¾ settings.event_issue_label_desc=工啿 ‡ç¾å·²æ›´æ–°æˆ–清除。 settings.event_issue_milestone=å·¥å•å·²æ”¶å…¥é‡Œç¨‹ç¢‘ä¸ settings.event_issue_milestone_desc=å·¥å•å·²æ”¶å…¥æˆ–å–æ¶ˆæ”¶å…¥é‡Œç¨‹ç¢‘ä¸ã€‚ diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index ae4ea7ea87..41e89ae567 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -701,18 +701,18 @@ func ContainerRoutes() *web.Router { r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) r.Group("/{username}", func() { r.PathGroup("/*", func(g *web.RouterPathGroup) { - g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) - g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagList) + g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) + g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { switch ctx.Req.Method { case http.MethodGet: - container.GetUploadBlob(ctx) + container.GetBlobsUpload(ctx) case http.MethodPatch: - container.UploadBlob(ctx) + container.PatchBlobsUpload(ctx) case http.MethodPut: - container.EndUploadBlob(ctx) + container.PutBlobsUpload(ctx) default: /* DELETE */ - container.CancelUploadBlob(ctx) + container.DeleteBlobsUpload(ctx) } }) g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) @@ -721,7 +721,7 @@ func ContainerRoutes() *web.Router { g.MatchPath("HEAD", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.HeadManifest) g.MatchPath("GET", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.GetManifest) - g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) + g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.PutManifest) g.MatchPath("DELETE", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) }) }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 4a2320ab76..2ea9b3839c 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -20,6 +20,8 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" + + "github.com/opencontainers/go-digest" ) // saveAsPackageBlob creates a package blob from an upload @@ -128,8 +130,8 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI pv := &packages_model.PackageVersion{ PackageID: p.ID, CreatorID: pi.Owner.ID, - Version: container_model.UploadVersion, - LowerVersion: container_model.UploadVersion, + Version: container_module.UploadVersion, + LowerVersion: container_module.UploadVersion, IsInternal: true, MetadataJSON: "null", } @@ -175,7 +177,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p return nil } -func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error { +func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) if err != nil { return err @@ -186,7 +188,7 @@ func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{ OwnerID: ownerID, Image: image, - Digest: digest, + Digest: string(digest), }) if err != nil { return err diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 2316657498..477c3bc71a 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -231,7 +231,7 @@ func GetRepositoryList(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func InitiateUploadBlob(ctx *context.Context) { +func PostBlobsUploads(ctx *context.Context) { image := ctx.PathParam("image") mount := ctx.FormTrim("mount") @@ -319,7 +319,7 @@ func InitiateUploadBlob(ctx *context.Context) { } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func GetUploadBlob(ctx *context.Context) { +func GetBlobsUpload(ctx *context.Context) { uuid := ctx.PathParam("uuid") upload, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -345,7 +345,7 @@ func GetUploadBlob(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func UploadBlob(ctx *context.Context) { +func PatchBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) @@ -393,7 +393,7 @@ func UploadBlob(ctx *context.Context) { } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func EndUploadBlob(ctx *context.Context) { +func PutBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") digest := ctx.FormTrim("digest") @@ -462,7 +462,7 @@ func EndUploadBlob(ctx *context.Context) { } // https://docs.docker.com/registry/spec/api/#delete-blob-upload -func CancelUploadBlob(ctx *context.Context) { +func DeleteBlobsUpload(ctx *context.Context) { uuid := ctx.PathParam("uuid") _, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -486,16 +486,15 @@ func CancelUploadBlob(ctx *context.Context) { } func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { return nil, container_model.ErrContainerBlobNotExist } return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), - Digest: d, + Digest: string(d), }) } @@ -535,9 +534,8 @@ func GetBlob(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs func DeleteBlob(ctx *context.Context) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { apiErrorDefined(ctx, errBlobUnknown) return } @@ -553,7 +551,7 @@ func DeleteBlob(ctx *context.Context) { } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests -func UploadManifest(ctx *context.Context) { +func PutManifest(ctx *context.Context) { reference := ctx.PathParam("reference") mci := &manifestCreationInfo{ @@ -609,18 +607,18 @@ func UploadManifest(ctx *context.Context) { } func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) { - reference := ctx.PathParam("reference") - opts := &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), IsManifest: true, } - if digest.Digest(reference).Validate() == nil { - opts.Digest = reference + reference := ctx.PathParam("reference") + if d := digest.Digest(reference); d.Validate() == nil { + opts.Digest = string(d) } else if referencePattern.MatchString(reference) { opts.Tag = reference + opts.OnlyLead = true } else { return nil, container_model.ErrContainerBlobNotExist } @@ -737,7 +735,7 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery -func GetTagList(ctx *context.Context) { +func GetTagsList(ctx *context.Context) { image := ctx.PathParam("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 26faa7b024..0cbd46e943 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -10,6 +10,7 @@ import ( "io" "os" "strings" + "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -23,19 +24,19 @@ import ( notify_service "code.gitea.io/gitea/services/notify" packages_service "code.gitea.io/gitea/services/packages" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" oci "github.com/opencontainers/image-spec/specs-go/v1" ) -func isValidMediaType(mt string) bool { +func isMediaTypeValid(mt string) bool { return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.") } -func isImageManifestMediaType(mt string) bool { +func isMediaTypeImageManifest(mt string) bool { return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json") } -func isImageIndexMediaType(mt string) bool { +func isMediaTypeImageIndex(mt string) bool { return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json") } @@ -64,22 +65,22 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag return "", err } - if !isValidMediaType(mci.MediaType) { + if !isMediaTypeValid(mci.MediaType) { mci.MediaType = index.MediaType - if !isValidMediaType(mci.MediaType) { + if !isMediaTypeValid(mci.MediaType) { return "", errManifestInvalid.WithMessage("MediaType not recognized") } } - if isImageManifestMediaType(mci.MediaType) { - return processImageManifest(ctx, mci, buf) - } else if isImageIndexMediaType(mci.MediaType) { - return processImageManifestIndex(ctx, mci, buf) + if isMediaTypeImageManifest(mci.MediaType) { + return processOciImageManifest(ctx, mci, buf) + } else if isMediaTypeImageIndex(mci.MediaType) { + return processOciImageIndex(ctx, mci, buf) } return "", errManifestInvalid } -func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { +func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { manifestDigest := "" err := func() error { @@ -150,13 +151,13 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p return err } - uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion) + uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_module.UploadVersion) if err != nil && err != packages_model.ErrPackageNotExist { return err } for _, ref := range blobReferences { - if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { + if _, err = createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { return err } } @@ -196,7 +197,7 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p return manifestDigest, nil } -func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { +func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { manifestDigest := "" err := func() error { @@ -221,7 +222,7 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b } for _, manifest := range index.Manifests { - if !isImageManifestMediaType(manifest.MediaType) { + if !isMediaTypeImageManifest(manifest.MediaType) { return errManifestInvalid } @@ -349,24 +350,31 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met LowerVersion: strings.ToLower(mci.Reference), MetadataJSON: string(metadataJSON), } - var pv *packages_model.PackageVersion - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + pv, err := packages_model.GetOrInsertVersion(ctx, _pv) + if err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return nil, err } - if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } - - // keep download count on overwrite - _pv.DownloadCount = pv.DownloadCount - - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { - log.Error("Error inserting package: %v", err) - return nil, err + if isMediaTypeImageIndex(mci.MediaType) { + if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) { + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + // keep download count on overwriting + _pv.DownloadCount = pv.DownloadCount + if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err + } + } + } else { + err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON}) + if err != nil { + return nil, err + } } } } @@ -376,14 +384,23 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } if mci.IsTagged { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { - log.Error("Error setting package version property: %v", err) + if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { return nil, err } + } else { + props, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged) + if err != nil { + return nil, err + } + for _, prop := range props { + if err = packages_model.DeletePropertyByID(ctx, prop.ID); err != nil { + return nil, err + } + } } + for _, manifest := range metadata.Manifests { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { - log.Error("Error setting package version property: %v", err) + if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { return nil, err } } @@ -400,9 +417,9 @@ type blobReference struct { IsLead bool } -func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) error { +func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) (*packages_model.PackageFile, error) { if ref.File.Blob.Size != ref.ExpectedSize { - return errSizeInvalid + return nil, errSizeInvalid } if ref.Name == "" { @@ -410,20 +427,21 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } pf := &packages_model.PackageFile{ - VersionID: pv.ID, - BlobID: ref.File.Blob.ID, - Name: ref.Name, - LowerName: ref.Name, - IsLead: ref.IsLead, + VersionID: pv.ID, + BlobID: ref.File.Blob.ID, + Name: ref.Name, + LowerName: ref.Name, + CompositeKey: string(ref.Digest), + IsLead: ref.IsLead, } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { if errors.Is(err, packages_model.ErrDuplicatePackageFile) { // Skip this blob because the manifest contains the same filesystem layer multiple times. - return nil + return pf, nil } log.Error("Error inserting package file: %v", err) - return err + return nil, err } props := map[string]string{ @@ -433,18 +451,18 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package for name, value := range props { if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil { log.Error("Error setting package file property: %v", err) - return err + return nil, err } } - // Remove the file from the blob upload version + // Remove the ref file (old file) from the blob upload version if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID { if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil { - return err + return nil, err } } - return nil + return pf, nil } func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) { @@ -471,14 +489,34 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack } manifestDigest := digestFromHashSummer(buf) - err = createFileFromBlobReference(ctx, pv, nil, &blobReference{ + pf, err := createFileFromBlobReference(ctx, pv, nil, &blobReference{ Digest: digest.Digest(manifestDigest), MediaType: mci.MediaType, - Name: container_model.ManifestFilename, + Name: container_module.ManifestFilename, File: &packages_model.PackageFileDescriptor{Blob: pb}, ExpectedSize: pb.Size, IsLead: true, }) + if err != nil { + return nil, false, "", err + } + oldManifestFiles, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: mci.Owner.ID, + PackageType: packages_model.TypeContainer, + VersionID: pv.ID, + Query: container_module.ManifestFilename, + }) + if err != nil { + return nil, false, "", err + } + for _, oldManifestFile := range oldManifestFiles { + if oldManifestFile.ID != pf.ID && oldManifestFile.IsLead { + err = packages_model.UpdateFile(ctx, &packages_model.PackageFile{ID: oldManifestFile.ID, IsLead: false}, []string{"is_lead"}) + if err != nil { + return nil, false, "", err + } + } + } return pb, !exists, manifestDigest, err } diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go index a726065ad0..801c60af13 100644 --- a/routers/api/packages/nuget/api_v2.go +++ b/routers/api/packages/nuget/api_v2.go @@ -246,21 +246,30 @@ type TypedValue[T any] struct { } type FeedEntryProperties struct { - Version string `xml:"d:Version"` - NormalizedVersion string `xml:"d:NormalizedVersion"` Authors string `xml:"d:Authors"` + Copyright string `xml:"d:Copyright,omitempty"` + Created TypedValue[time.Time] `xml:"d:Created"` Dependencies string `xml:"d:Dependencies"` Description string `xml:"d:Description"` - VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` + DevelopmentDependency TypedValue[bool] `xml:"d:DevelopmentDependency"` DownloadCount TypedValue[int64] `xml:"d:DownloadCount"` - PackageSize TypedValue[int64] `xml:"d:PackageSize"` - Created TypedValue[time.Time] `xml:"d:Created"` + ID string `xml:"d:Id"` + IconURL string `xml:"d:IconUrl,omitempty"` + Language string `xml:"d:Language,omitempty"` LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"` - Published TypedValue[time.Time] `xml:"d:Published"` + LicenseURL string `xml:"d:LicenseUrl,omitempty"` + MinClientVersion string `xml:"d:MinClientVersion,omitempty"` + NormalizedVersion string `xml:"d:NormalizedVersion"` + Owners string `xml:"d:Owners,omitempty"` + PackageSize TypedValue[int64] `xml:"d:PackageSize"` ProjectURL string `xml:"d:ProjectUrl,omitempty"` + Published TypedValue[time.Time] `xml:"d:Published"` ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"` RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"` - Title string `xml:"d:Title"` + Tags string `xml:"d:Tags,omitempty"` + Title string `xml:"d:Title,omitempty"` + Version string `xml:"d:Version"` + VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` } type FeedEntry struct { @@ -353,21 +362,30 @@ func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNames Author: metadata.Authors, Content: content, Properties: &FeedEntryProperties{ - Version: pd.Version.Version, - NormalizedVersion: pd.Version.Version, Authors: metadata.Authors, + Copyright: metadata.Copyright, + Created: createdValue, Dependencies: buildDependencyString(metadata), Description: metadata.Description, - VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, + DevelopmentDependency: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.DevelopmentDependency}, DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, - PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, - Created: createdValue, + ID: pd.Package.Name, + IconURL: metadata.IconURL, + Language: metadata.Language, LastUpdated: createdValue, - Published: createdValue, + LicenseURL: metadata.LicenseURL, + MinClientVersion: metadata.MinClientVersion, + NormalizedVersion: pd.Version.Version, + Owners: metadata.Owners, + PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, ProjectURL: metadata.ProjectURL, + Published: createdValue, ReleaseNotes: metadata.ReleaseNotes, RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance}, - Title: pd.Package.Name, + Tags: metadata.Tags, + Title: metadata.Title, + Version: pd.Version.Version, + VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, }, } diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index de8c7ef3ed..cb880c8bdb 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -14,6 +14,7 @@ import ( "strings" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" @@ -309,7 +310,7 @@ func GetPackageInfo(ctx *context.Context) { apiError(ctx, http.StatusNotFound, nil) return } - infoContent, err := makePackageInfo(ctx, versions) + infoContent, err := makePackageInfo(ctx, versions, cache.NewEphemeralCache()) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -317,7 +318,7 @@ func GetPackageInfo(ctx *context.Context) { ctx.PlainText(http.StatusOK, infoContent) } -// GetAllPackagesVersions returns a custom text based format containing information about all versions of all rubygems. +// GetAllPackagesVersions returns a custom text-based format containing information about all versions of all rubygems. // ref: https://guides.rubygems.org/rubygems-org-compact-index-api/ func GetAllPackagesVersions(ctx *context.Context) { packages, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems) @@ -326,6 +327,7 @@ func GetAllPackagesVersions(ctx *context.Context) { return } + ephemeralCache := cache.NewEphemeralCache() out := &strings.Builder{} out.WriteString("---\n") for _, pkg := range packages { @@ -338,7 +340,7 @@ func GetAllPackagesVersions(ctx *context.Context) { continue } - info, err := makePackageInfo(ctx, versions) + info, err := makePackageInfo(ctx, versions, ephemeralCache) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -348,7 +350,14 @@ func GetAllPackagesVersions(ctx *context.Context) { _, _ = fmt.Fprintf(out, "%s ", pkg.Name) for i, v := range versions { sep := util.Iif(i == len(versions)-1, "", ",") - _, _ = fmt.Fprintf(out, "%s%s", v.Version, sep) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, v, ephemeralCache) + if errors.Is(err, util.ErrNotExist) { + continue + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + writePackageVersionForList(pd.Metadata, v.Version, sep, out) } _, _ = fmt.Fprintf(out, " %x\n", md5.Sum([]byte(info))) } @@ -356,6 +365,16 @@ func GetAllPackagesVersions(ctx *context.Context) { ctx.PlainText(http.StatusOK, out.String()) } +func writePackageVersionForList(metadata any, version, sep string, out *strings.Builder) { + if metadata, _ := metadata.(*rubygems_module.Metadata); metadata != nil && metadata.Platform != "" && metadata.Platform != "ruby" { + // VERSION_PLATFORM (see comment above in GetAllPackagesVersions) + _, _ = fmt.Fprintf(out, "%s_%s%s", version, metadata.Platform, sep) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s%s", version, sep) + } +} + func writePackageVersionRequirements(prefix string, reqs []rubygems_module.VersionRequirement, out *strings.Builder) { out.WriteString(prefix) if len(reqs) == 0 { @@ -367,11 +386,21 @@ func writePackageVersionRequirements(prefix string, reqs []rubygems_module.Versi } } -func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion) (string, error) { +func writePackageVersionForDependency(version, platform string, out *strings.Builder) { + if platform != "" && platform != "ruby" { + // VERSION-PLATFORM (see comment below in makePackageVersionDependency) + _, _ = fmt.Fprintf(out, "%s-%s ", version, platform) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s ", version) + } +} + +func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { // format: VERSION[-PLATFORM] [DEPENDENCY[,DEPENDENCY,...]]|REQUIREMENT[,REQUIREMENT,...] // DEPENDENCY: GEM:CONSTRAINT[&CONSTRAINT] // REQUIREMENT: KEY:VALUE (always contains "checksum") - pd, err := packages_model.GetPackageDescriptor(ctx, version) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, version, c) if err != nil { return "", err } @@ -388,8 +417,7 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. } buf := &strings.Builder{} - buf.WriteString(version.Version) - buf.WriteByte(' ') + writePackageVersionForDependency(version.Version, metadata.Platform, buf) for i, dep := range metadata.RuntimeDependencies { sep := util.Iif(i == 0, "", ",") writePackageVersionRequirements(fmt.Sprintf("%s%s:", sep, dep.Name), dep.Version, buf) @@ -404,10 +432,10 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. return buf.String(), nil } -func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion) (string, error) { +func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { ret := "---\n" for _, v := range versions { - dep, err := makePackageVersionDependency(ctx, v) + dep, err := makePackageVersionDependency(ctx, v, c) if err != nil { return "", err } diff --git a/routers/api/packages/rubygems/rubygems_test.go b/routers/api/packages/rubygems/rubygems_test.go new file mode 100644 index 0000000000..a07e12a7d3 --- /dev/null +++ b/routers/api/packages/rubygems/rubygems_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package rubygems + +import ( + "strings" + "testing" + + rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" + + "github.com/stretchr/testify/assert" +) + +func TestWritePackageVersion(t *testing.T) { + buf := &strings.Builder{} + + writePackageVersionForList(nil, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "ruby"}, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "linux"}, "1.0", " ", buf) + assert.Equal(t, "1.0_linux ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "ruby", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "os", buf) + assert.Equal(t, "1.0-os ", buf.String()) + buf.Reset() +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 8b99dd95da..de34a9375c 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -575,7 +575,13 @@ func PrepareCompareDiff( ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link() ctx.Data["AfterCommitID"] = headCommitID - ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") + + // follow GitHub's behavior: autofill the form and expand + newPrFormTitle := ctx.FormTrim("title") + newPrFormBody := ctx.FormTrim("body") + ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") || ctx.FormBool("quick_pull") || newPrFormTitle != "" || newPrFormBody != "" + ctx.Data["TitleQuery"] = newPrFormTitle + ctx.Data["BodyQuery"] = newPrFormBody if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) || headCommitID == ci.CompareInfo.BaseCommitID { diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index cbcb3a3b21..62bf8b182f 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -145,10 +145,6 @@ func editFile(ctx *context.Context, isNewFile bool) { } blob := entry.Blob() - if blob.Size() >= setting.UI.MaxDisplayFileSize { - ctx.NotFound(err) - return - } buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) if err != nil { @@ -162,22 +158,37 @@ func editFile(ctx *context.Context, isNewFile bool) { defer dataRc.Close() - ctx.Data["FileSize"] = blob.Size() - - // Only some file types are editable online as text. - if !fInfo.st.IsRepresentableAsText() || fInfo.isLFSFile { - ctx.NotFound(nil) - return + if fInfo.isLFSFile { + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) + if err != nil { + ctx.ServerError("GetTreePathLock", err) + return + } + if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + ctx.NotFound(nil) + return + } } - d, _ := io.ReadAll(dataRc) + ctx.Data["FileSize"] = fInfo.fileSize - buf = append(buf, d...) - if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { - log.Error("ToUTF8: %v", err) - ctx.Data["FileContent"] = string(buf) + // Only some file types are editable online as text. + if fInfo.isLFSFile { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") + } else if !fInfo.st.IsRepresentableAsText() { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") + } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file") } else { - ctx.Data["FileContent"] = content + d, _ := io.ReadAll(dataRc) + + buf = append(buf, d...) + if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { + log.Error("ToUTF8: %v", err) + ctx.Data["FileContent"] = string(buf) + } else { + ctx.Data["FileContent"] = content + } } } else { // Append filename from query, or empty string to allow username the new file. @@ -280,6 +291,10 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b operation := "update" if isNewFile { operation = "create" + } else if !form.Content.Has() && ctx.Repo.TreePath != form.TreePath { + // The form content only has data if file is representable as text, is not too large and not in lfs. If it doesn't + // have data, the only possible operation is a rename + operation = "rename" } if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ @@ -292,7 +307,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b Operation: operation, FromTreePath: ctx.Repo.TreePath, TreePath: form.TreePath, - ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")), + ContentReader: strings.NewReader(strings.ReplaceAll(form.Content.Value(), "\r", "")), }, }, Signoff: form.Signoff, diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index ca346b7e6c..3ffd8f89c4 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -99,7 +99,7 @@ func NewDiffPatchPost(ctx *context.Context) { OldBranch: ctx.Repo.BranchName, NewBranch: branchName, Message: message, - Content: strings.ReplaceAll(form.Content, "\r", ""), + Content: strings.ReplaceAll(form.Content.Value(), "\r", ""), Author: gitCommitter, Committer: gitCommitter, }) diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index f43433fb0d..ec0ad02828 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -285,10 +285,10 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } - prepareToRenderButtons(ctx, fInfo.isLFSFile, isRepresentableAsText, lfsLock) + prepareToRenderButtons(ctx, lfsLock) } -func prepareToRenderButtons(ctx *context.Context, isLFSFile, isRepresentableAsText bool, lfsLock *git_model.LFSLock) { +func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { // archived or mirror repository, the buttons should not be shown if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() { return @@ -301,33 +301,16 @@ func prepareToRenderButtons(ctx *context.Context, isLFSFile, isRepresentableAsTe return } - if isLFSFile { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") - } else if !isRepresentableAsText { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") - } - if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - if !isLFSFile { // lfs file cannot be edited after fork - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") - } + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") return } // it's a lfs file and the user is not the owner of the lock - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.Data["CanEditFile"] = false - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - ctx.Data["CanDeleteFile"] = false - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - return - } - - if !isLFSFile { // lfs file cannot be edited - ctx.Data["CanEditFile"] = true - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") - } - ctx.Data["CanDeleteFile"] = true - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") + isLFSLocked := lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID + ctx.Data["CanEditFile"] = !isLFSLocked + ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file")) + ctx.Data["CanDeleteFile"] = !isLFSLocked + ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file")) } diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index d0139f6613..73f9970a07 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -245,7 +245,12 @@ func TestDefaultWikiBranch(t *testing.T) { assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main")) // repo with wiki - assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"})) + assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime( + db.DefaultContext, + &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}, + "default_wiki_branch", + ), + ) ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki") ctx.SetPathParam("*", "Home") diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index a2827e516a..d11ad0a54c 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -10,6 +10,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/context" @@ -689,7 +690,7 @@ func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.E // EditRepoFileForm form for changing repository file type EditRepoFileForm struct { TreePath string `binding:"Required;MaxSize(500)"` - Content string + Content optional.Option[string] CommitSummary string `binding:"MaxSize(100)"` CommitMessage string CommitChoice string `binding:"Required;MaxSize(50)"` diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index 3f5f43bbc0..d15d6b6c84 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -57,7 +57,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e Type: packages_model.TypeContainer, Version: packages_model.SearchValue{ ExactMatch: true, - Value: container_model.UploadVersion, + Value: container_module.UploadVersion, }, IsInternal: optional.Some(true), HasFiles: optional.Some(false), diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 3b958e0d4c..2bd1c55de4 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -196,8 +196,13 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr return fmt.Errorf("setDefaultBranch: %w", err) } } - if err = updateRepository(ctx, repo, false); err != nil { - return fmt.Errorf("updateRepository: %w", err) + + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_empty", "default_branch"); err != nil { + return fmt.Errorf("UpdateRepositoryCols: %w", err) + } + + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) } return nil diff --git a/services/repository/create.go b/services/repository/create.go index 83d7d84c08..6f918b2d24 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -191,10 +191,14 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re } } - if err = UpdateRepository(ctx, repo, false); err != nil { + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_empty", "default_branch", "default_wiki_branch"); err != nil { return fmt.Errorf("updateRepository: %w", err) } + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + return nil } diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 712914a27e..e1acf6a92f 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -246,7 +246,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use contentStore := lfs.NewContentStore() for _, file := range opts.Files { switch file.Operation { - case "create", "update": + case "create", "update", "rename": if err := CreateOrUpdateFile(ctx, t, file, contentStore, repo.ID, hasOldBranch); err != nil { return nil, err } @@ -488,31 +488,32 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file } } - treeObjectContentReader := file.ContentReader - var lfsMetaObject *git_model.LFSMetaObject - if setting.LFS.StartServer && hasOldBranch { - // Check there is no way this can return multiple infos - attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ - Attributes: []string{attribute.Filter}, - Filenames: []string{file.Options.treePath}, - }) + var oldEntry *git.TreeEntry + // Assume that the file.ContentReader of a pure rename operation is invalid. Use the file content how it's present in + // git instead + if file.Operation == "rename" { + lastCommitID, err := t.GetLastCommit(ctx) + if err != nil { + return err + } + commit, err := t.GetCommit(lastCommitID) if err != nil { return err } - if attributesMap[file.Options.treePath] != nil && attributesMap[file.Options.treePath].Get(attribute.Filter).ToString().Value() == "lfs" { - // OK so we are supposed to LFS this data! - pointer, err := lfs.GeneratePointer(treeObjectContentReader) - if err != nil { - return err - } - lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID} - treeObjectContentReader = strings.NewReader(pointer.StringContent()) + if oldEntry, err = commit.GetTreeEntryByPath(file.Options.fromTreePath); err != nil { + return err } } - // Add the object to the database - objectHash, err := t.HashObject(ctx, treeObjectContentReader) + var objectHash string + var lfsPointer *lfs.Pointer + switch file.Operation { + case "create", "update": + objectHash, lfsPointer, err = createOrUpdateFileHash(ctx, t, file, hasOldBranch) + case "rename": + objectHash, lfsPointer, err = renameFileHash(ctx, t, oldEntry, file) + } if err != nil { return err } @@ -528,9 +529,9 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file } } - if lfsMetaObject != nil { + if lfsPointer != nil { // We have an LFS object - create it - lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject.RepositoryID, lfsMetaObject.Pointer) + lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, *lfsPointer) if err != nil { return err } @@ -539,11 +540,20 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file return err } if !exist { - _, err := file.ContentReader.Seek(0, io.SeekStart) - if err != nil { - return err + var lfsContentReader io.Reader + if file.Operation != "rename" { + if _, err := file.ContentReader.Seek(0, io.SeekStart); err != nil { + return err + } + lfsContentReader = file.ContentReader + } else { + if lfsContentReader, err = oldEntry.Blob().DataAsync(); err != nil { + return err + } + defer lfsContentReader.(io.ReadCloser).Close() } - if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil { + + if err := contentStore.Put(lfsMetaObject.Pointer, lfsContentReader); err != nil { if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil { return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) } @@ -555,6 +565,99 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file return nil } +func createOrUpdateFileHash(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, hasOldBranch bool) (string, *lfs.Pointer, error) { + treeObjectContentReader := file.ContentReader + var lfsPointer *lfs.Pointer + if setting.LFS.StartServer && hasOldBranch { + // Check there is no way this can return multiple infos + attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ + Attributes: []string{attribute.Filter}, + Filenames: []string{file.Options.treePath}, + }) + if err != nil { + return "", nil, err + } + + if attributesMap[file.Options.treePath] != nil && attributesMap[file.Options.treePath].Get(attribute.Filter).ToString().Value() == "lfs" { + // OK so we are supposed to LFS this data! + pointer, err := lfs.GeneratePointer(treeObjectContentReader) + if err != nil { + return "", nil, err + } + lfsPointer = &pointer + treeObjectContentReader = strings.NewReader(pointer.StringContent()) + } + } + + // Add the object to the database + objectHash, err := t.HashObject(ctx, treeObjectContentReader) + if err != nil { + return "", nil, err + } + + return objectHash, lfsPointer, nil +} + +func renameFileHash(ctx context.Context, t *TemporaryUploadRepository, oldEntry *git.TreeEntry, file *ChangeRepoFile) (string, *lfs.Pointer, error) { + if setting.LFS.StartServer { + attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ + Attributes: []string{attribute.Filter}, + Filenames: []string{file.Options.treePath, file.Options.fromTreePath}, + }) + if err != nil { + return "", nil, err + } + + oldIsLfs := attributesMap[file.Options.fromTreePath] != nil && attributesMap[file.Options.fromTreePath].Get(attribute.Filter).ToString().Value() == "lfs" + newIsLfs := attributesMap[file.Options.treePath] != nil && attributesMap[file.Options.treePath].Get(attribute.Filter).ToString().Value() == "lfs" + + // If the old and new paths are both in lfs or both not in lfs, the object hash of the old file can be used directly + // as the object doesn't change + if oldIsLfs == newIsLfs { + return oldEntry.ID.String(), nil, nil + } + + oldEntryReader, err := oldEntry.Blob().DataAsync() + if err != nil { + return "", nil, err + } + defer oldEntryReader.Close() + + var treeObjectContentReader io.Reader + var lfsPointer *lfs.Pointer + // If the old path is in lfs but the new isn't, read the content from lfs and add it as normal git object + // If the new path is in lfs but the old isn't, read the content from the git object and generate a lfs + // pointer of it + if oldIsLfs { + pointer, err := lfs.ReadPointer(oldEntryReader) + if err != nil { + return "", nil, err + } + treeObjectContentReader, err = lfs.ReadMetaObject(pointer) + if err != nil { + return "", nil, err + } + defer treeObjectContentReader.(io.ReadCloser).Close() + } else { + pointer, err := lfs.GeneratePointer(oldEntryReader) + if err != nil { + return "", nil, err + } + treeObjectContentReader = strings.NewReader(pointer.StringContent()) + lfsPointer = &pointer + } + + // Add the object to the database + objectID, err := t.HashObject(ctx, treeObjectContentReader) + if err != nil { + return "", nil, err + } + return objectID, lfsPointer, nil + } + + return oldEntry.ID.String(), nil, nil +} + // VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName string, treePaths []string) error { protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) diff --git a/services/repository/fork.go b/services/repository/fork.go index bd1554f163..d0568e6072 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -209,7 +209,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork // ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Repository) error { - err := db.WithTx(ctx, func(ctx context.Context) error { + return db.WithTx(ctx, func(ctx context.Context) error { repo, err := repo_model.GetRepositoryByID(ctx, repo.ID) if err != nil { return err @@ -226,16 +226,8 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit repo.IsFork = false repo.ForkID = 0 - - if err := updateRepository(ctx, repo, false); err != nil { - log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err) - return err - } - - return nil + return repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_fork", "fork_id") }) - - return err } type findForksOptions struct { diff --git a/services/repository/generate.go b/services/repository/generate.go index 77a43b4e39..867b5d7855 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -253,43 +253,35 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) } -func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) { - tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-" + repo.Name) +// GenerateGitContent generates git content from a template repository +func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) (err error) { + tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-" + generateRepo.Name) if err != nil { - return fmt.Errorf("failed to create temp dir for repository %s: %w", repo.FullName(), err) + return fmt.Errorf("failed to create temp dir for repository %s: %w", generateRepo.FullName(), err) } defer cleanup() - if err = generateRepoCommit(ctx, repo, templateRepo, generateRepo, tmpDir); err != nil { + if err = generateRepoCommit(ctx, generateRepo, templateRepo, generateRepo, tmpDir); err != nil { return fmt.Errorf("generateRepoCommit: %w", err) } // re-fetch repo - if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { + if generateRepo, err = repo_model.GetRepositoryByID(ctx, generateRepo.ID); err != nil { return fmt.Errorf("getRepositoryByID: %w", err) } // if there was no default branch supplied when generating the repo, use the default one from the template - if strings.TrimSpace(repo.DefaultBranch) == "" { - repo.DefaultBranch = templateRepo.DefaultBranch + if strings.TrimSpace(generateRepo.DefaultBranch) == "" { + generateRepo.DefaultBranch = templateRepo.DefaultBranch } - if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { + if err = gitrepo.SetDefaultBranch(ctx, generateRepo, generateRepo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } - if err = UpdateRepository(ctx, repo, false); err != nil { + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, generateRepo, "default_branch"); err != nil { return fmt.Errorf("updateRepository: %w", err) } - return nil -} - -// GenerateGitContent generates git content from a template repository -func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { - if err := generateGitContent(ctx, generateRepo, templateRepo, generateRepo); err != nil { - return err - } - if err := repo_module.UpdateRepoSize(ctx, generateRepo); err != nil { return fmt.Errorf("failed to update size for repository: %w", err) } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 0859158b89..0a3dc45339 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -220,10 +220,14 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } repo.IsMirror = true - if err = UpdateRepository(ctx, repo, false); err != nil { + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "num_watches", "is_empty", "default_branch", "default_wiki_branch", "is_mirror"); err != nil { return nil, err } + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + // this is necessary for sync local tags from remote configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName()) if stdout, _, err := git.NewCommand("config"). diff --git a/services/repository/repository.go b/services/repository/repository.go index 739ef1ec38..0cdce336d4 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -127,9 +127,42 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error) { return db.WithTx(ctx, func(ctx context.Context) error { repo.IsPrivate = false - if err = updateRepository(ctx, repo, true); err != nil { - return fmt.Errorf("MakeRepoPublic: %w", err) + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { + return err + } + + if err = repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("LoadOwner: %w", err) } + if repo.Owner.IsOrganization() { + // Organization repository need to recalculate access table when visibility is changed. + if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { + return fmt.Errorf("recalculateTeamAccesses: %w", err) + } + } + + // Create/Remove git-daemon-export-ok for git-daemon... + if err := checkDaemonExportOK(ctx, repo); err != nil { + return err + } + + forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID) + if err != nil { + return fmt.Errorf("getRepositoriesByForkID: %w", err) + } + + if repo.Owner.Visibility != structs.VisibleTypePrivate { + for i := range forkRepos { + if err = MakeRepoPublic(ctx, forkRepos[i]); err != nil { + return fmt.Errorf("MakeRepoPublic[%d]: %w", forkRepos[i].ID, err) + } + } + } + + // If visibility is changed, we need to update the issue indexer. + // Since the data in the issue indexer have field to indicate if the repo is public or not. + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + return nil }) } @@ -137,9 +170,51 @@ func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err error) { return db.WithTx(ctx, func(ctx context.Context) error { repo.IsPrivate = true - if err = updateRepository(ctx, repo, true); err != nil { - return fmt.Errorf("MakeRepoPrivate: %w", err) + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { + return err + } + + if err = repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("LoadOwner: %w", err) } + if repo.Owner.IsOrganization() { + // Organization repository need to recalculate access table when visibility is changed. + if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { + return fmt.Errorf("recalculateTeamAccesses: %w", err) + } + } + + // If repo has become private, we need to set its actions to private. + _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{ + IsPrivate: true, + }) + if err != nil { + return err + } + + if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil { + return err + } + + // Create/Remove git-daemon-export-ok for git-daemon... + if err := checkDaemonExportOK(ctx, repo); err != nil { + return err + } + + forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID) + if err != nil { + return fmt.Errorf("getRepositoriesByForkID: %w", err) + } + for i := range forkRepos { + if err = MakeRepoPrivate(ctx, forkRepos[i]); err != nil { + return fmt.Errorf("MakeRepoPrivate[%d]: %w", forkRepos[i].ID, err) + } + } + + // If visibility is changed, we need to update the issue indexer. + // Since the data in the issue indexer have field to indicate if the repo is public or not. + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + return nil }) } diff --git a/tailwind.config.js b/tailwind.config.js index fe285432f3..01740d816b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -33,7 +33,6 @@ export default { '!./templates/swagger/v1_json.tmpl', '!./templates/user/auth/oidc_wellknown.tmpl', '!**/*_test.go', - '!./modules/{public,options,templates}/bindata.go', './{build,models,modules,routers,services}/**/*.go', './templates/**/*.tmpl', './web_src/js/**/*.{ts,js,vue}', diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 7d89f8c6e2..b4e12cf26b 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -16,7 +16,17 @@ </div> <div class="field"> <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.container.digest"}}</label> - <div class="markup"><pre class="code-block"><code>{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}</code></pre></div> + <div class="markup"> + <div class="code-block-container code-overflow-scroll"> + <pre class="code-block"><code> + {{- range .PackageDescriptor.Files -}} + {{- if eq .File.LowerName "manifest.json" -}} + {{- .Properties.GetByName "container.digest" -}}{{"\n"}} + {{- end -}} + {{- end -}} + </code></pre> + </div> + </div> </div> <div class="field"> <label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/"}}</label> diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index fa96d2f0e2..22abf9a219 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -148,7 +148,7 @@ <a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> {{else}} <a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> - {{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}} + {{if and $.Repository.CanEnableEditor $.CanEditFile}} <a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a> {{end}} {{end}} diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 8f46c47b96..7efed77349 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -77,7 +77,7 @@ </div> {{end}} </div> - <button id="commit-button" type="submit" class="ui primary button"> + <button id="commit-button" type="submit" class="ui primary button" {{if .PageIsEdit}}disabled{{end}}> {{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}} </button> <a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a> diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index ae8a60c20c..e1bf46d53d 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -28,31 +28,40 @@ <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> </div> </div> - <div class="field"> - <div class="ui top attached header"> - <div class="ui compact small menu small-menu-items repo-editor-menu"> - <a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a> - <a class="item" data-tab="preview" data-preview-url="{{.Repository.Link}}/markup" data-preview-context-ref="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a> - {{if not .IsNewFile}} - <a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a> - {{end}} - </div> - </div> - <div class="ui bottom attached segment tw-p-0"> - <div class="ui active tab tw-rounded-b" data-tab="write"> - <textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" - data-previewable-extensions="{{.PreviewableExtensions}}" - data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea> - <div class="editor-loading is-loading"></div> + {{if not .NotEditableReason}} + <div class="field"> + <div class="ui top attached header"> + <div class="ui compact small menu small-menu-items repo-editor-menu"> + <a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a> + <a class="item" data-tab="preview" data-preview-url="{{.Repository.Link}}/markup" data-preview-context-ref="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a> + {{if not .IsNewFile}} + <a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a> + {{end}} + </div> </div> - <div class="ui tab tw-px-4 tw-py-3" data-tab="preview"> - {{ctx.Locale.Tr "loading"}} + <div class="ui bottom attached segment tw-p-0"> + <div class="ui active tab tw-rounded-b" data-tab="write"> + <textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" + data-previewable-extensions="{{.PreviewableExtensions}}" + data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea> + <div class="editor-loading is-loading"></div> + </div> + <div class="ui tab tw-px-4 tw-py-3" data-tab="preview"> + {{ctx.Locale.Tr "loading"}} + </div> + <div class="ui tab" data-tab="diff"> + <div class="tw-p-16"></div> + </div> </div> - <div class="ui tab" data-tab="diff"> - <div class="tw-p-16"></div> + </div> + {{else}} + <div class="field"> + <div class="ui segment tw-text-center"> + <h4 class="tw-font-semibold tw-mb-2">{{.NotEditableReason}}</h4> + <p>{{ctx.Locale.Tr "repo.editor.file_not_editable_hint"}}</p> </div> </div> - </div> + {{end}} {{template "repo/editor/commit_form" .}} </form> </div> diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 773a9cb8ef..4bcb13c448 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -18,7 +18,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" - container_model "code.gitea.io/gitea/models/packages/container" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" container_module "code.gitea.io/gitea/modules/packages/container" @@ -58,7 +57,7 @@ func TestPackageContainer(t *testing.T) { return values } - images := []string{"test", "te/st"} + images := []string{"test", "sub/name"} tags := []string{"latest", "main"} multiTag := "multi" @@ -71,7 +70,8 @@ func TestPackageContainer(t *testing.T) { configContent := `{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/true"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"container":"b89fe92a887d55c0961f02bdfbfd8ac3ddf66167db374770d2d9e9fab3311510","container_config":{"Hostname":"b89fe92a887d","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/true\"]"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"created":"2022-01-01T00:00:00.000000000Z","docker_version":"20.10.12","history":[{"created":"2022-01-01T00:00:00.000000000Z","created_by":"/bin/sh -c #(nop) COPY file:0e7589b0c800daaf6fa460d2677101e4676dd9491980210cb345480e513f3602 in /true "},{"created":"2022-01-01T00:00:00.000000001Z","created_by":"/bin/sh -c #(nop) CMD [\"/true\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0ff3b91bdf21ecdf2f2f3d4372c2098a14dbe06cd678e8f0a85fd4902d00e2e2"]}}` manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6" - manifestContent := `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` + manifestContent := `{"schemaVersion":2,"mediaType":"` + container_module.ContentTypeDockerDistributionManifestV2 + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` + manifestContentType := container_module.ContentTypeDockerDistributionManifestV2 untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d" untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` @@ -251,7 +251,7 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location")) assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) - pv, err := packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, container_model.UploadVersion) + pv, err := packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, container_module.UploadVersion) assert.NoError(t, err) pfs, err := packages_model.GetFilesByVersionID(db.DefaultContext, pv.ID) @@ -431,7 +431,7 @@ func TestPackageContainer(t *testing.T) { assert.Len(t, pd.Files, 3) for _, pfd := range pd.Files { switch pfd.File.Name { - case container_model.ManifestFilename: + case container_module.ManifestFilename: assert.True(t, pfd.File.IsLead) assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", pfd.Properties.GetByName(container_module.PropertyMediaType)) assert.Equal(t, manifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) @@ -494,7 +494,7 @@ func TestPackageContainer(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, strconv.Itoa(len(manifestContent)), resp.Header().Get("Content-Length")) - assert.Equal(t, oci.MediaTypeImageManifest, resp.Header().Get("Content-Type")) + assert.Equal(t, manifestContentType, resp.Header().Get("Content-Type")) assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, manifestContent, resp.Body.String()) }) @@ -533,7 +533,7 @@ func TestPackageContainer(t *testing.T) { assert.Len(t, pd.Files, 3) for _, pfd := range pd.Files { - if pfd.File.Name == container_model.ManifestFilename { + if pfd.File.Name == container_module.ManifestFilename { assert.True(t, pfd.File.IsLead) assert.Equal(t, oci.MediaTypeImageManifest, pfd.Properties.GetByName(container_module.PropertyMediaType)) assert.Equal(t, untaggedManifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) @@ -562,7 +562,8 @@ func TestPackageContainer(t *testing.T) { assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) - assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference)) + // only the last manifest digest is associated with the version (OCI builders will push the index manifest digest as the final step) + assert.ElementsMatch(t, []string{untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference)) assert.IsType(t, &container_module.Metadata{}, pd.Metadata) metadata := pd.Metadata.(*container_module.Metadata) diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index c0e69a82cd..65b1b9845a 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -46,21 +46,30 @@ func TestPackageNuGet(t *testing.T) { defer tests.PrepareTestEnv(t)() type FeedEntryProperties struct { - Version string `xml:"Version"` - NormalizedVersion string `xml:"NormalizedVersion"` Authors string `xml:"Authors"` + Copyright string `xml:"Copyright,omitempty"` + Created nuget.TypedValue[time.Time] `xml:"Created"` Dependencies string `xml:"Dependencies"` Description string `xml:"Description"` - VersionDownloadCount nuget.TypedValue[int64] `xml:"VersionDownloadCount"` + DevelopmentDependency nuget.TypedValue[bool] `xml:"DevelopmentDependency"` DownloadCount nuget.TypedValue[int64] `xml:"DownloadCount"` - PackageSize nuget.TypedValue[int64] `xml:"PackageSize"` - Created nuget.TypedValue[time.Time] `xml:"Created"` + ID string `xml:"Id"` + IconURL string `xml:"IconUrl,omitempty"` + Language string `xml:"Language,omitempty"` LastUpdated nuget.TypedValue[time.Time] `xml:"LastUpdated"` - Published nuget.TypedValue[time.Time] `xml:"Published"` + LicenseURL string `xml:"LicenseUrl,omitempty"` + MinClientVersion string `xml:"MinClientVersion,omitempty"` + NormalizedVersion string `xml:"NormalizedVersion"` + Owners string `xml:"Owners,omitempty"` + PackageSize nuget.TypedValue[int64] `xml:"PackageSize"` ProjectURL string `xml:"ProjectUrl,omitempty"` + Published nuget.TypedValue[time.Time] `xml:"Published"` ReleaseNotes string `xml:"ReleaseNotes,omitempty"` RequireLicenseAcceptance nuget.TypedValue[bool] `xml:"RequireLicenseAcceptance"` + Tags string `xml:"Tags,omitempty"` Title string `xml:"Title"` + Version string `xml:"Version"` + VersionDownloadCount nuget.TypedValue[int64] `xml:"VersionDownloadCount"` } type FeedEntry struct { @@ -86,28 +95,54 @@ func TestPackageNuGet(t *testing.T) { readToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadPackage) badToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadNotification) - packageName := "test.package" + packageName := "test.package" // id + packageID := packageName packageVersion := "1.0.3" packageAuthors := "KN4CK3R" packageDescription := "Gitea Test Package" + symbolFilename := "test.pdb" symbolID := "d910bb6948bd4c6cb40155bcf52c3c94" + packageCopyright := "Package Copyright" + packageIconURL := "https://gitea.io/favicon.png" + packageLanguage := "Package Language" + packageLicenseURL := "https://gitea.io/license" + packageMinClientVersion := "1.0.0.0" + packageOwners := "Package Owners" + packageProjectURL := "https://gitea.io" + packageReleaseNotes := "Package Release Notes" + packageTags := "tag_1 tag_2 tag_3" + packageTitle := "Package Title" + packageDevelopmentDependency := true + packageRequireLicenseAcceptance := true + createNuspec := func(id, version string) string { return `<?xml version="1.0" encoding="utf-8"?> -<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> - <metadata> - <id>` + id + `</id> - <version>` + version + `</version> - <authors>` + packageAuthors + `</authors> - <description>` + packageDescription + `</description> - <dependencies> - <group targetFramework=".NETStandard2.0"> - <dependency id="Microsoft.CSharp" version="4.5.0" /> - </group> - </dependencies> - </metadata> -</package>` + <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata minClientVersion="` + packageMinClientVersion + `"> + <authors>` + packageAuthors + `</authors> + <copyright>` + packageCopyright + `</copyright> + <description>` + packageDescription + `</description> + <developmentDependency>true</developmentDependency> + <iconUrl>` + packageIconURL + `</iconUrl> + <id>` + id + `</id> + <language>` + packageLanguage + `</language> + <licenseUrl>` + packageLicenseURL + `</licenseUrl> + <owners>` + packageOwners + `</owners> + <projectUrl>` + packageProjectURL + `</projectUrl> + <releaseNotes>` + packageReleaseNotes + `</releaseNotes> + <requireLicenseAcceptance>true</requireLicenseAcceptance> + <tags>` + packageTags + `</tags> + <title>` + packageTitle + `</title> + <version>` + version + `</version> + <dependencies> + <group targetFramework=".NETStandard2.0"> + <dependency id="Microsoft.CSharp" version="4.5.0" /> + </group> + </dependencies> + </metadata> + </package>` } createPackage := func(id, version string) *bytes.Buffer { @@ -393,7 +428,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID) assert.NoError(t, err) - assert.Equal(t, int64(412), pb.Size) + assert.Equal(t, int64(610), pb.Size) case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion): assert.False(t, pf.IsLead) @@ -405,7 +440,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID) assert.NoError(t, err) - assert.Equal(t, int64(427), pb.Size) + assert.Equal(t, int64(996), pb.Size) case symbolFilename: assert.False(t, pf.IsLead) @@ -736,10 +771,24 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) var result FeedEntry decodeXML(t, resp, &result) - assert.Equal(t, packageName, result.Properties.Title) - assert.Equal(t, packageVersion, result.Properties.Version) assert.Equal(t, packageAuthors, result.Properties.Authors) assert.Equal(t, packageDescription, result.Properties.Description) + assert.Equal(t, packageID, result.Properties.ID) + assert.Equal(t, packageVersion, result.Properties.Version) + + assert.Equal(t, packageCopyright, result.Properties.Copyright) + assert.Equal(t, packageDevelopmentDependency, result.Properties.DevelopmentDependency.Value) + assert.Equal(t, packageIconURL, result.Properties.IconURL) + assert.Equal(t, packageLanguage, result.Properties.Language) + assert.Equal(t, packageLicenseURL, result.Properties.LicenseURL) + assert.Equal(t, packageMinClientVersion, result.Properties.MinClientVersion) + assert.Equal(t, packageOwners, result.Properties.Owners) + assert.Equal(t, packageProjectURL, result.Properties.ProjectURL) + assert.Equal(t, packageReleaseNotes, result.Properties.ReleaseNotes) + assert.Equal(t, packageRequireLicenseAcceptance, result.Properties.RequireLicenseAcceptance.Value) + assert.Equal(t, packageTags, result.Properties.Tags) + assert.Equal(t, packageTitle, result.Properties.Title) + assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies) }) diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go index 786addbd76..b6a79940cb 100644 --- a/tests/integration/api_packages_test.go +++ b/tests/integration/api_packages_test.go @@ -15,9 +15,9 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" - container_model "code.gitea.io/gitea/models/packages/container" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -538,7 +538,7 @@ func TestPackageCleanup(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, pbs) - _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_model.UploadVersion) + _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_module.UploadVersion) assert.NoError(t, err) err = packages_cleanup_service.CleanupTask(db.DefaultContext, duration) @@ -548,7 +548,7 @@ func TestPackageCleanup(t *testing.T) { assert.NoError(t, err) assert.Empty(t, pbs) - _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_model.UploadVersion) + _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_module.UploadVersion) assert.ErrorIs(t, err, packages_model.ErrPackageNotExist) }) diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index ce55a2f943..4678e52a9c 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -58,6 +58,50 @@ func getUpdateRepoFilesOptions(repo *repo_model.Repository) *files_service.Chang } } +func getUpdateRepoFilesRenameOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { + return &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + // move normally + { + Operation: "rename", + FromTreePath: "README.md", + TreePath: "README.txt", + SHA: "", + ContentReader: nil, + }, + // move from in lfs + { + Operation: "rename", + FromTreePath: "crypt.bin", + TreePath: "crypt1.bin", + SHA: "", + ContentReader: nil, + }, + // move from lfs to normal + { + Operation: "rename", + FromTreePath: "jpeg.jpg", + TreePath: "jpeg.jpeg", + SHA: "", + ContentReader: nil, + }, + // move from normal to lfs + { + Operation: "rename", + FromTreePath: "CONTRIBUTING.md", + TreePath: "CONTRIBUTING.md.bin", + SHA: "", + ContentReader: nil, + }, + }, + OldBranch: repo.DefaultBranch, + NewBranch: repo.DefaultBranch, + Message: "Rename files", + Author: nil, + Committer: nil, + } +} + func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { return &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ @@ -248,6 +292,109 @@ func getExpectedFileResponseForRepoFilesUpdate(commitID, filename, lastCommitSHA } } +func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA string, lastCommitterWhen, lastAuthorWhen time.Time) *api.FilesResponse { + details := []map[string]any{ + { + "filename": "README.txt", + "sha": "8276d2a29779af982c0afa976bdb793b52d442a8", + "size": 22, + "content": "IyBBbiBMRlMtZW5hYmxlZCByZXBvCg==", + }, + { + "filename": "crypt1.bin", + "sha": "d4a41a0d4db4949e129bd22f871171ea988103ef", + "size": 129, + "content": "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6MmVjY2RiNDM4MjVkMmE0OWQ5OWQ1NDJkYWEyMDA3NWNmZjFkOTdkOWQyMzQ5YTg5NzdlZmU5YzAzNjYxNzM3YwpzaXplIDIwNDgK", + }, + { + "filename": "jpeg.jpeg", + "sha": "71911bf48766c7181518c1070911019fbb00b1fc", + "size": 107, + "content": "/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=", + }, + { + "filename": "CONTRIBUTING.md.bin", + "sha": "2b6c6c4eaefa24b22f2092c3d54b263ff26feb58", + "size": 127, + "content": "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6N2I2YjJjODhkYmE5Zjc2MGExYTU4NDY5YjY3ZmVlMmI2OThlZjdlOTM5OWM0Y2E0ZjM0YTE0Y2NiZTM5ZjYyMwpzaXplIDI3Cg==", + }, + } + + var responses []*api.ContentsResponse + for _, detail := range details { + encoding := "base64" + content := detail["content"].(string) + selfURL := setting.AppURL + "api/v1/repos/user2/lfs/contents/" + detail["filename"].(string) + "?ref=master" + htmlURL := setting.AppURL + "user2/lfs/src/branch/master/" + detail["filename"].(string) + gitURL := setting.AppURL + "api/v1/repos/user2/lfs/git/blobs/" + detail["sha"].(string) + downloadURL := setting.AppURL + "user2/lfs/raw/branch/master/" + detail["filename"].(string) + + responses = append(responses, &api.ContentsResponse{ + Name: detail["filename"].(string), + Path: detail["filename"].(string), + SHA: detail["sha"].(string), + LastCommitSHA: lastCommitSHA, + LastCommitterDate: lastCommitterWhen, + LastAuthorDate: lastAuthorWhen, + Type: "file", + Size: int64(detail["size"].(int)), + Encoding: &encoding, + Content: &content, + URL: &selfURL, + HTMLURL: &htmlURL, + GitURL: &gitURL, + DownloadURL: &downloadURL, + Links: &api.FileLinksResponse{ + Self: &selfURL, + GitURL: &gitURL, + HTMLURL: &htmlURL, + }, + }) + } + + return &api.FilesResponse{ + Files: responses, + Commit: &api.FileCommitResponse{ + CommitMeta: api.CommitMeta{ + URL: setting.AppURL + "api/v1/repos/user2/lfs/git/commits/" + commitID, + SHA: commitID, + }, + HTMLURL: setting.AppURL + "user2/lfs/commit/" + commitID, + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "User Two", + Email: "user2@noreply.example.org", + }, + Date: time.Now().UTC().Format(time.RFC3339), + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "User Two", + Email: "user2@noreply.example.org", + }, + Date: time.Now().UTC().Format(time.RFC3339), + }, + Parents: []*api.CommitMeta{ + { + URL: setting.AppURL + "api/v1/repos/user2/lfs/git/commits/73cf03db6ece34e12bf91e8853dc58f678f2f82d", + SHA: "73cf03db6ece34e12bf91e8853dc58f678f2f82d", + }, + }, + Message: "Rename files\n", + Tree: &api.CommitMeta{ + URL: setting.AppURL + "api/v1/repos/user2/lfs/git/trees/5307376dc3a5557dc1c403c29a8984668ca9ecb5", + SHA: "5307376dc3a5557dc1c403c29a8984668ca9ecb5", + }, + }, + Verification: &api.PayloadCommitVerification{ + Verified: false, + Reason: "gpg.error.not_signed_commit", + Signature: "", + Payload: "", + }, + } +} + func TestChangeRepoFilesForCreate(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { @@ -369,6 +516,35 @@ func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) { }) } +func TestChangeRepoFilesForUpdateWithFileRename(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + ctx, _ := contexttest.MockContext(t, "user2/lfs") + ctx.SetPathParam("id", "54") + contexttest.LoadRepo(t, ctx, 54) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + + repo := ctx.Repo.Repository + doer := ctx.Doer + opts := getUpdateRepoFilesRenameOptions(repo) + + // test + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, doer, opts) + + // asserts + assert.NoError(t, err) + gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) + defer gitRepo.Close() + + commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch) + lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath) + expectedFileResponse := getExpectedFileResponseForRepoFilesUpdateRename(commit.ID.String(), lastCommit.ID.String(), lastCommit.Committer.When, lastCommit.Author.When) + assert.Equal(t, expectedFileResponse, filesResponse) + }) +} + // Test opts with branch names removed, should get same results as above test func TestChangeRepoFilesWithoutBranchNames(t *testing.T) { // setup diff --git a/web_src/js/features/comp/EditorUpload.test.ts b/web_src/js/features/comp/EditorUpload.test.ts index 55f3f74389..e6e5f4de13 100644 --- a/web_src/js/features/comp/EditorUpload.test.ts +++ b/web_src/js/features/comp/EditorUpload.test.ts @@ -1,4 +1,4 @@ -import {removeAttachmentLinksFromMarkdown} from './EditorUpload.ts'; +import {pasteAsMarkdownLink, removeAttachmentLinksFromMarkdown} from './EditorUpload.ts'; test('removeAttachmentLinksFromMarkdown', () => { expect(removeAttachmentLinksFromMarkdown('a foo b', 'foo')).toBe('a foo b'); @@ -12,3 +12,13 @@ test('removeAttachmentLinksFromMarkdown', () => { expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo"> b', 'foo')).toBe('a b'); expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo" width="100"/> b', 'foo')).toBe('a b'); }); + +test('preparePasteAsMarkdownLink', () => { + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 0}, 'bar')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 0}, 'https://gitea.com')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 3}, 'bar')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 3}, 'https://gitea.com')).toBe('[foo](https://gitea.com)'); + expect(pasteAsMarkdownLink({value: '..(url)', selectionStart: 3, selectionEnd: 6}, 'https://gitea.com')).toBe('[url](https://gitea.com)'); + expect(pasteAsMarkdownLink({value: '[](url)', selectionStart: 3, selectionEnd: 6}, 'https://gitea.com')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'https://example.com', selectionStart: 0, selectionEnd: 19}, 'https://gitea.com')).toBeNull(); +}); diff --git a/web_src/js/features/comp/EditorUpload.ts b/web_src/js/features/comp/EditorUpload.ts index f6d5731422..3f6d26658d 100644 --- a/web_src/js/features/comp/EditorUpload.ts +++ b/web_src/js/features/comp/EditorUpload.ts @@ -118,17 +118,26 @@ export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string return text; } -function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, text: string, isShiftDown: boolean) { +export function pasteAsMarkdownLink(textarea: {value: string, selectionStart: number, selectionEnd: number}, pastedText: string): string | null { + const {value, selectionStart, selectionEnd} = textarea; + const selectedText = value.substring(selectionStart, selectionEnd); + const trimmedText = pastedText.trim(); + const beforeSelection = value.substring(0, selectionStart); + const afterSelection = value.substring(selectionEnd); + const isInMarkdownLink = beforeSelection.endsWith('](') && afterSelection.startsWith(')'); + const asMarkdownLink = selectedText && isUrl(trimmedText) && !isUrl(selectedText) && !isInMarkdownLink; + return asMarkdownLink ? `[${selectedText}](${trimmedText})` : null; +} + +function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, pastedText: string, isShiftDown: boolean) { // pasting with "shift" means "paste as original content" in most applications if (isShiftDown) return; // let the browser handle it // when pasting links over selected text, turn it into [text](link) - const {value, selectionStart, selectionEnd} = textarea; - const selectedText = value.substring(selectionStart, selectionEnd); - const trimmedText = text.trim(); - if (selectedText && isUrl(trimmedText) && !isUrl(selectedText)) { + const pastedAsMarkdown = pasteAsMarkdownLink(textarea, pastedText); + if (pastedText) { e.preventDefault(); - replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`); + replaceTextareaSelection(textarea, pastedAsMarkdown); } // else, let the browser handle it } diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index 0f77508f70..acf4127399 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -141,38 +141,39 @@ export function initRepoEditor() { } }); + const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form'); + + // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage + // to enable or disable the commit button + const commitButton = document.querySelector<HTMLButtonElement>('#commit-button'); + const dirtyFileClass = 'dirty-file'; + + // Enabling the button at the start if the page has posted + if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]')?.value === 'true') { + commitButton.disabled = false; + } + + // Registering a custom listener for the file path and the file content + // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added + applyAreYouSure(elForm, { + silent: true, + dirtyClass: dirtyFileClass, + fieldSelector: ':input:not(.commit-form-wrapper :input)', + change($form: any) { + const dirty = $form[0]?.classList.contains(dirtyFileClass); + commitButton.disabled = !dirty; + }, + }); + // on the upload page, there is no editor(textarea) const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area'); if (!editArea) return; - const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form'); initEditPreviewTab(elForm); (async () => { const editor = await createCodeEditor(editArea, filenameInput); - // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage - // to enable or disable the commit button - const commitButton = document.querySelector<HTMLButtonElement>('#commit-button'); - const dirtyFileClass = 'dirty-file'; - - // Disabling the button at the start - if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]').value !== 'true') { - commitButton.disabled = true; - } - - // Registering a custom listener for the file path and the file content - // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added - applyAreYouSure(elForm, { - silent: true, - dirtyClass: dirtyFileClass, - fieldSelector: ':input:not(.commit-form-wrapper :input)', - change($form: any) { - const dirty = $form[0]?.classList.contains(dirtyFileClass); - commitButton.disabled = !dirty; - }, - }); - // Update the editor from query params, if available, // only after the dirtyFileClass initialization const params = new URLSearchParams(window.location.search); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 0360b8ef95..02fee5a267 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -72,10 +72,10 @@ function updateSelectionLabel(label: HTMLElement) { } function onAfterFiltered(this: any) { - const $dropdown = $(this); + const $dropdown = $(this).closest('.ui.dropdown'); // "this" can be the "ui dropdown" or "<select>" const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty'; const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); - if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); + if (hideEmptyDividers && itemsMenu) hideScopedEmptyDividers(itemsMenu); } // delegate the dropdown's template functions and callback functions to add aria attributes. |