diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b32631d6a7..aea31640b32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,14 @@ # Contribution Guidelines +This document explains how to contribute changes to the Gitea project. Topic-specific guides live in separate files so the essentials are easier to find. + +| Topic | Document | +| :---- | :------- | +| Backend (Go modules, API v1) | [docs/guideline-backend.md](docs/guideline-backend.md) | +| Frontend (npm, UI guidelines) | [docs/guideline-frontend.md](docs/guideline-frontend.md) | +| Maintainers, TOC, labels, merge queue, commit format for mergers | [docs/community-governance.md](docs/community-governance.md) | +| Release cycle, backports, tagging releases | [docs/release-management.md](docs/release-management.md) | +
Table of Contents - [Contribution Guidelines](#contribution-guidelines) @@ -11,10 +20,6 @@ - [Discuss your design before the implementation](#discuss-your-design-before-the-implementation) - [Issue locking](#issue-locking) - [Building Gitea](#building-gitea) - - [Dependencies](#dependencies) - - [Backend](#backend) - - [Frontend](#frontend) - - [Design guideline](#design-guideline) - [Styleguide](#styleguide) - [Copyright](#copyright) - [Testing](#testing) @@ -22,47 +27,19 @@ - [Code review](#code-review) - [Pull request format](#pull-request-format) - [PR title and summary](#pr-title-and-summary) - - [Milestone](#milestone) - - [Labels](#labels) - [Breaking PRs](#breaking-prs) - [What is a breaking PR?](#what-is-a-breaking-pr) - [How to handle breaking PRs?](#how-to-handle-breaking-prs) - [Maintaining open PRs](#maintaining-open-prs) - - [Getting PRs merged](#getting-prs-merged) - - [Final call](#final-call) - - [Commit messages](#commit-messages) - - [PR Co-authors](#pr-co-authors) - - [PRs targeting `main`](#prs-targeting-main) - - [Backport PRs](#backport-prs) + - [Reviewing PRs](#reviewing-prs) + - [For PR authors](#for-pr-authors) - [Documentation](#documentation) - - [API v1](#api-v1) - - [GitHub API compatibility](#github-api-compatibility) - - [Adding/Maintaining API routes](#addingmaintaining-api-routes) - - [When to use what HTTP method](#when-to-use-what-http-method) - - [Requirements for API routes](#requirements-for-api-routes) - - [Backports and Frontports](#backports-and-frontports) - - [What is backported?](#what-is-backported) - - [How to backport?](#how-to-backport) - - [Format of backport PRs](#format-of-backport-prs) - - [Frontports](#frontports) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) - - [Release Cycle](#release-cycle) - - [Maintainers](#maintainers) - - [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc) - - [TOC election process](#toc-election-process) - - [Current TOC members](#current-toc-members) - - [Previous TOC/owners members](#previous-tocowners-members) - - [Governance Compensation](#governance-compensation) - - [TOC \& Working groups](#toc--working-groups) - - [Roadmap](#roadmap) - - [Versions](#versions) - - [Releasing Gitea](#releasing-gitea)
## Introduction -This document explains how to contribute changes to the Gitea project. \ It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \ Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io). @@ -131,34 +108,6 @@ If further discussion is needed, we encourage you to open a new issue instead an See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea). -## Dependencies - -### Backend - -Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \ -You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules). - -Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \ -Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies. - -The `go.mod`, `go.sum` update needs to be justified as part of the PR description, -and must be verified by the reviewers and/or merger to always reference -an existing upstream commit. - -### Frontend - -For the frontend, we use [npm](https://www.npmjs.com/). - -The same restrictions apply for frontend dependencies as for backend dependencies, with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version. - -## Design guideline - -Depending on your change, please read the - -- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) -- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend) -- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring) - ## Styleguide You should always run `make fmt` before committing to conform to Gitea's styleguide. @@ -216,6 +165,8 @@ The tool `go run build/backport-locale.go` can be used to backport locales from ## Code review +How labels, milestones, and the merge queue work is documented in [docs/community-governance.md](docs/community-governance.md). + ### Pull request format Please try to make your pull request easy to review for us. \ @@ -260,29 +211,6 @@ Fixes/Closes/Resolves #. to your summary. \ Each issue that will be closed must stand on a separate line. -### Milestone - -A PR should only be assigned to a milestone if it will likely be merged into the given version. \ -As a rule of thumb, assume that a PR will stay open for an additional month for every 100 added lines. \ -PRs without a milestone may not be merged. - -### Labels - -Almost all labels used inside Gitea can be classified as one of the following: - -- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI. -- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually. -- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one). -- `issue/…` / `pr/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `pr/need-2-approvals` - -Every PR should be labeled correctly with every label that applies. - -There are also some labels that will be managed automatically.\ -In particular, these are - -- the amount of pending required approvals -- has all `backport`s or needs a manual backport - ### Breaking PRs #### What is a breaking PR? @@ -311,165 +239,29 @@ Breaking PRs will not be merged as long as not both of these requirements are me ### Maintaining open PRs -The moment you create a non-draft PR or the moment you convert a draft PR to a non-draft PR is the moment code review starts for it. \ -Once that happens, do not rebase or squash your branch anymore as it makes it difficult to review the new changes. \ -Merge the base branch into your branch only when you really need to, i.e. because of conflicting changes in the mean time. \ -This reduces unnecessary CI runs. \ -Don't worry about merge commits messing up your commit history as every PR will be squash merged. \ -This means that all changes are joined into a single new commit whose message is as described below. +Code review starts when you open a non-draft PR or move a draft out of draft state. After that, do not rebase or squash your branch; it makes new changes harder to review. -### Getting PRs merged +Merge the base branch into yours only when you need to, for example because of conflicting changes elsewhere. That limits unnecessary CI runs. -Changes to Gitea must be reviewed before they are accepted — no matter who -makes the change, even if they are an owner or a maintainer. \ -The only exception are critical bugs that prevent Gitea from being compiled or started. \ -Specifically, we require two approvals from maintainers for every PR. \ -Once this criteria has been met, your PR receives the `lgtm/done` label. \ -From this point on, your only responsibility is to fix merge conflicts or respond to/implement requests by maintainers. \ -It is the responsibility of the maintainers from this point to get your PR merged. +Every PR is squash-merged, so merge commits on your branch do not matter for final history. The squash produces a single commit; mergers follow the [commit message format](docs/community-governance.md#commit-messages) in the governance guide. -If a PR has the `lgtm/done` label and there are no open discussions or merge conflicts anymore, any maintainer can add the `reviewed/wait-merge` label. \ -This label means that the PR is part of the merge queue and will be merged as soon as possible. \ -The merge queue will be cleared in the order of the list below: +### Reviewing PRs - +Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest. -Gitea uses it's own tool, the to automate parts of the review process. \ -This tool does the things listed below automatically: +#### For PR authors -- create a backport PR if needed once the initial PR was merged -- remove the PR from the merge queue after the PR merged -- keep the oldest branch in the merge queue up to date with merges +- **Response**: When answering reviewer questions, use real-world cases or examples and avoid speculation. +- **Discussion**: A discussion is always welcome and should be used to clarify the changes and the intent of the PR. +- **Help**: If you need help with the PR or comments are unclear, ask for clarification. -### Final call - -If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment. - -After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously. - -However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author). - -### Commit messages - -Mergers are able and required to rewrite the PR title and summary (the first comment of a PR) so that it can produce an easily understandable commit message if necessary. \ -The final commit message should no longer contain any uncertainty such as `hopefully, won't happen anymore`. Replace uncertainty with certainty. - -#### PR Co-authors - -A person counts as a PR co-author the moment they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. \ -Mergers are required to remove such "false-positive" co-authors when writing the commit message. \ -The true co-authors must remain in the commit message. - -#### PRs targeting `main` - -The commit message of PRs targeting `main` is always - -```bash -$PR_TITLE ($PR_INDEX) - -$REWRITTEN_PR_SUMMARY -``` - -#### Backport PRs - -The commit message of backport PRs is always - -```bash -$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX) - -$REWRITTEN_PR_SUMMARY -``` +Guidance for reviewers, the merge queue, and the squash commit message format is in [docs/community-governance.md](docs/community-governance.md). ## Documentation If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs). **The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.** -## API v1 - -The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest). - -### GitHub API compatibility - -Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \ -If Gitea provides functionality that GitHub does not, a new endpoint can be created. \ -If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \ -Updating an existing API should not remove existing fields unless there is a really good reason to do so. \ -The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned). - -### Adding/Maintaining API routes - -All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \ -All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \ -and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \ -They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \ -All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \ -and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \ -They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279). - -### When to use what HTTP method - -In general, HTTP methods are chosen as follows: - -- **GET** endpoints return the requested object(s) and status **OK (200)** -- **DELETE** endpoints return the status **No Content (204)** and no content either -- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object -- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either -- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)** - -### Requirements for API routes - -All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required). - -Endpoints returning lists must - -- support pagination (`page` & `limit` options in query) -- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) - -## Backports and Frontports - -### What is backported? - -We backport PRs given the following circumstances: - -1. Feature freeze is active, but `-rc0` has not been released yet. Here, we backport as much as possible. -2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. -3. We never backport new features. -4. We never backport breaking changes except when - 1. The breaking change has no effect on the vast majority of users - 2. The component triggering the breaking change is marked as experimental - -### How to backport? - -In the past, it was necessary to manually backport your PRs. \ -Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR - -- does not have the label `backport/manual` -- has the label `backport/` - -The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself. - -### Format of backport PRs - -The title of backport PRs should be - -``` - (#) -``` - -The first two lines of the summary of the backporting PR should be - -``` -Backport # - -``` - -with the rest of the summary and labels matching the original PR. - -### Frontports - -Frontports behave exactly as described above for backports. - ## Developer Certificate of Origin (DCO) We consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). \ @@ -483,148 +275,3 @@ Signed-off-by: Joe Smith If you set the `user.name` and `user.email` Git config options, you can add the line to the end of your commits automatically with `git commit -s`. We assume in good faith that the information you provide is legally binding. - -## Release Cycle - -We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \ -The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \ -All the feature pull requests should be -merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding -release branch is open for fixes backported from main branch. Release candidates -are made during this period for user testing to -obtain a final version that is maintained in this branch. - -During a development cycle, we may also publish any necessary minor releases -for the previous version. For example, if the latest, published release is -v1.2, then minor changes for the previous release—e.g., v1.1.0 -> v1.1.1—are -still possible. - -## Maintainers - -To make sure every PR is checked, we have [maintainers](MAINTAINERS). \ -Every PR **must** be reviewed by at least two maintainers (or owners) before it can get merged. \ -For refactoring PRs after a week and documentation only PRs, the approval of only one maintainer is enough. \ -A maintainer should be a contributor of Gitea and contributed at least -4 accepted PRs. A contributor should apply as a maintainer in the -[Discord](https://discord.gg/Gitea) `#develop` channel. The team maintainers may invite the contributor. A maintainer -should spend some time on code reviews. If a maintainer has no -time to do that, they should apply to leave the maintainers team -and we will give them the honor of being a member of the [advisors -team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if -an advisor has time to code review, we will gladly welcome them back -to the maintainers team. If a maintainer is inactive for more than 3 -months and forgets to leave the maintainers team, the owners may move -him or her from the maintainers team to the advisors team. -For security reasons, Maintainers should use 2FA for their accounts and -if possible provide GPG signed commits. -https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ -https://help.github.com/articles/signing-commits-with-gpg/ - -Furthermore, any account with write access (like bots and TOC members) **must** use 2FA. -https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ - -## Technical Oversight Committee (TOC) - -At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company. -https://blog.gitea.com/quarterly-23q1/ - -### TOC election process - -Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company. -A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election. -If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate. - -The TOC is elected for one year, the TOC election happens yearly. -After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat. -If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat. -Refusals result in the person with the next highest vote getting the same choice. -As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat. - -If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration. - -### Current TOC members - -- 2024-01-01 ~ 2024-12-31 - - Company - - [Jason Song](https://gitea.com/wolfogre) - - [Lunny Xiao](https://gitea.com/lunny) - - [Matti Ranta](https://gitea.com/techknowlogick) - - Community - - [6543](https://gitea.com/6543) <6543@obermui.de> - - [delvh](https://gitea.com/delvh) - - [John Olheiser](https://gitea.com/jolheiser) - -### Previous TOC/owners members - -Here's the history of the owners and the time they served: - -- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017 -- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017 -- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801) -- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [6543](https://gitea.com/6543) - 2023 -- [John Olheiser](https://gitea.com/jolheiser) - 2023 -- [Jason Song](https://gitea.com/wolfogre) - 2023 - -## Governance Compensation - -Each member of the community elected TOC will be granted $500 each month as compensation for their work. - -Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release. - -These funds will come from community sources like the OpenCollective rather than directly from the company. -Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties. -Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself. - -## TOC & Working groups - -With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places. - -The currently proposed groups are: - -- **Core Group**: maintain the primary Gitea repository -- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc. -- **Documentation Group**: maintain related documents and repositories -- **Translation Group**: coordinate with translators and maintain translations -- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items - -## Roadmap - -Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders. -TOC members need to review the roadmap every year and work together on the direction of the project. - -When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion. - -You can visit our roadmap on the wiki. - -## Versions - -Gitea has the `main` branch as a tip branch and has version branches -such as `release/v1.19`. `release/v1.19` is a release branch and we will -tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept -pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag, -after bringing the bug fix also to the main branch. - -Since the `main` branch is a tip version, if you wish to use Gitea -in production, please download the latest release tag version. All the -branches will be protected via GitHub, all the PRs to every branch must -be reviewed by two maintainers and must pass the automatic tests. - -## Releasing Gitea - -- Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future. -- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours. -- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps: - - Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`. - - When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin` -- If it is bugfix version create PR for changelog on branch `release/v$vmaj.$vmin` and wait till it is reviewed and merged. -- Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`. -- And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) -- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. -- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. -- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: - - bump the version of https://dl.gitea.com/gitea/version.json - - merge the blog post PR - - announce the release in discord `#announcements` diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c714ed96600..ef276e4da58 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -41,10 +41,10 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; App name that shows in every page title -APP_NAME = ; Gitea: Git with a cup of tea +;APP_NAME = Gitea: Git with a cup of tea ;; ;; RUN_USER will automatically detect the current user - but you can set it here change it if you run locally -RUN_USER = ; git +;RUN_USER = ;; ;; Application run mode, affects performance and debugging: "dev" or "prod", default is "prod" ;; Mode "dev" makes Gitea easier to develop and debug, values other than "dev" are treated as "prod" which is for production use. diff --git a/docs/community-governance.md b/docs/community-governance.md new file mode 100644 index 00000000000..dbf24813295 --- /dev/null +++ b/docs/community-governance.md @@ -0,0 +1,198 @@ +# Community governance and review process + +This document describes maintainer expectations, project governance, and the detailed pull request review workflow (labels, merge queue, commit message format for mergers). For what contributors should do when opening and updating a PR, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Code review + +### Milestone + +A PR should only be assigned to a milestone if it will likely be merged into the given version. \ +PRs without a milestone may not be merged. + +### Labels + +Almost all labels used inside Gitea can be classified as one of the following: + +- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI. +- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually. +- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one). +- `issue/…` / `lgtm/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `lgtm/need 2` + +Every PR should be labeled correctly with every label that applies. + +There are also some labels that will be managed automatically.\ +In particular, these are + +- the amount of pending required approvals +- has all `backport`s or needs a manual backport + +### Reviewing PRs + +Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest. + +#### For reviewers + +- **Verification**: Verify that the PR accurately reflects the changes, and verify that the tests and documentation are complete and aligned with the implementation. +- **Actionable feedback**: Say what should change and why, and distinguish required changes from optional suggestions. +- **Feedback**: Focus feedback on the issue itself and avoid comments about the contributor's abilities. +- **Request changes**: If you request changes (i.e., block a PR), give a clear rationale and, whenever possible, a concrete path to resolution. +- **Approval**: Only approve a PR when you are fully satisfied with its current state - "rubber-stamp" approvals need to be highlighted as such. + +### Getting PRs merged + +Changes to Gitea must be reviewed before they are accepted, including changes from owners and maintainers. The exception is critical bugs that prevent Gitea from compiling or starting. + +We require two maintainer approvals for every PR. When that is satisfied, your PR gets the `lgtm/done` label. After that, you mainly fix merge conflicts and respond to or implement maintainer requests; maintainers drive getting the PR merged. + +If a PR has `lgtm/done`, no open discussions, and no merge conflicts, any maintainer may add `reviewed/wait-merge`. That puts the PR in the merge queue. PRs are merged from the queue in the order of this list: + + + +Gitea uses its own tool, , to automate parts of the review process. The backporter: + +- Creates a backport PR when needed after the initial PR merges. +- Removes the PR from the merge queue after it merges. +- Keeps the oldest branch in the merge queue up to date with merges. + +### Final call + +If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment. + +After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously. + +However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author). + +### Commit messages + +Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear. + +The final commit message should not hedge: replace phrases like `hopefully, won't happen anymore` with definite wording. + +#### PR Co-authors + +A person counts as a PR co-author once they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. Mergers must remove such false-positive co-authors when writing the squash message. Every true co-author must remain in the commit message. + +#### PRs targeting `main` + +The commit message of PRs targeting `main` is always + +```bash +$PR_TITLE ($PR_INDEX) + +$REWRITTEN_PR_SUMMARY +``` + +#### Backport PRs + +The commit message of backport PRs is always + +```bash +$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX) + +$REWRITTEN_PR_SUMMARY +``` + +## Maintainers + +We list [maintainers](../MAINTAINERS) so every PR gets proper review. + +#### Review expectations + +Every PR **must** be reviewed by at least two maintainers (or owners) before merge. **Exception:** after one week, refactoring PRs and documentation-only PRs need only one maintainer approval. + +Maintainers are expected to spend time on code reviews. + +#### Becoming a maintainer + +A maintainer should already be a Gitea contributor with at least four merged PRs. To apply, use the [Discord](https://discord.gg/Gitea) `#develop` channel. Maintainer teams may also invite contributors. + +#### Stepping down, advisors, and inactivity + +If you cannot keep reviewing, apply to leave the maintainers team. You can join the [advisors team](https://github.com/orgs/go-gitea/teams/advisors); advisors who want to review again are welcome back as maintainers. + +If a maintainer is inactive for more than three months and has not left the team, owners may move them to the advisors team. + +#### Account security + +For security, maintainers should enable 2FA and sign commits with GPG when possible: + +- [Two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication) +- [Signing commits with GPG](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) + +Any account with write access (including bots and TOC members) **must** use [2FA](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). + +## Technical Oversight Committee (TOC) + +At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company. +https://blog.gitea.com/quarterly-23q1/ + +### TOC election process + +Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company. +A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election. +If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate. + +The TOC is elected for one year, the TOC election happens yearly. +After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat. +If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat. +Refusals result in the person with the next highest vote getting the same choice. +As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat. + +If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration. + +### Current TOC members + +- 2024-01-01 ~ 2024-12-31 + - Company + - [Jason Song](https://gitea.com/wolfogre) + - [Lunny Xiao](https://gitea.com/lunny) + - [Matti Ranta](https://gitea.com/techknowlogick) + - Community + - [6543](https://gitea.com/6543) <6543@obermui.de> + - [delvh](https://gitea.com/delvh) + - [John Olheiser](https://gitea.com/jolheiser) + +### Previous TOC/owners members + +Here's the history of the owners and the time they served: + +- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017 +- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017 +- [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801) +- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [6543](https://gitea.com/6543) - 2023 +- [John Olheiser](https://gitea.com/jolheiser) - 2023 +- [Jason Song](https://gitea.com/wolfogre) - 2023 + +## Governance Compensation + +Each member of the community elected TOC will be granted $500 each month as compensation for their work. + +Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release. + +These funds will come from community sources like the OpenCollective rather than directly from the company. +Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties. +Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself. + +## TOC & Working groups + +With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places. + +The currently proposed groups are: + +- **Core Group**: maintain the primary Gitea repository +- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc. +- **Documentation Group**: maintain related documents and repositories +- **Translation Group**: coordinate with translators and maintain translations +- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items + +## Roadmap + +Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders. +TOC members need to review the roadmap every year and work together on the direction of the project. + +When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion. + +You can visit our roadmap on the wiki. diff --git a/docs/guideline-backend.md b/docs/guideline-backend.md new file mode 100644 index 00000000000..bc3e71113f2 --- /dev/null +++ b/docs/guideline-backend.md @@ -0,0 +1,58 @@ +# Backend development + +This document covers backend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +For coding style and architecture, see also the [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) on the documentation site. + +## Dependencies + +Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \ +You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules). + +Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \ +Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies. + +The `go.mod`, `go.sum` update needs to be justified as part of the PR description, +and must be verified by the reviewers and/or merger to always reference +an existing upstream commit. + +## API v1 + +The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest). + +### GitHub API compatibility + +Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \ +If Gitea provides functionality that GitHub does not, a new endpoint can be created. \ +If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \ +Updating an existing API should not remove existing fields unless there is a really good reason to do so. \ +The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned). + +### Adding/Maintaining API routes + +All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \ +All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \ +and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \ +They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \ +All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \ +and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \ +They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279). + +### When to use what HTTP method + +In general, HTTP methods are chosen as follows: + +- **GET** endpoints return the requested object(s) and status **OK (200)** +- **DELETE** endpoints return the status **No Content (204)** and no content either +- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object +- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either +- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)** + +### Requirements for API routes + +All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required). + +Endpoints returning lists must + +- support pagination (`page` & `limit` options in query) +- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) diff --git a/docs/guideline-frontend.md b/docs/guideline-frontend.md new file mode 100644 index 00000000000..80ebe821777 --- /dev/null +++ b/docs/guideline-frontend.md @@ -0,0 +1,17 @@ +# Frontend development + +This document covers frontend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Dependencies + +For the frontend, we use [npm](https://www.npmjs.com/). + +The same restrictions apply for frontend dependencies as for [backend dependencies](guideline-backend.md#dependencies), with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version. + +## Design guideline + +Depending on your change, please read the + +- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) +- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend) +- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring) diff --git a/docs/release-management.md b/docs/release-management.md new file mode 100644 index 00000000000..be8d9e1abf2 --- /dev/null +++ b/docs/release-management.md @@ -0,0 +1,115 @@ +# Release management + +This document describes the release cycle, backports, versioning, and the release manager checklist. For everyday contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Backports and Frontports + +### What is backported? + +We backport PRs given the following circumstances: + +1. Feature freeze is active, but `-rc0` has not been released yet. Here, we backport as much as possible. +2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. +3. We never backport new features. +4. We never backport breaking changes except when + 1. The breaking change has no effect on the vast majority of users + 2. The component triggering the breaking change is marked as experimental + +### How to backport? + +In the past, it was necessary to manually backport your PRs. \ +Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR + +- does not have the label `backport/manual` +- has the label `backport/` + +The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself. + +### Format of backport PRs + +The title of backport PRs should be + +``` + (#) +``` + +The first two lines of the summary of the backporting PR should be + +``` +Backport # + +``` + +with the rest of the summary and labels matching the original PR. + +### Frontports + +Frontports behave exactly as described above for backports. + +## Release Cycle + +We use a release schedule so work, stabilization, and releases stay predictable. + +### Cadence + +- Aim for a major release about every three or four months. +- Roughly two or three months of general development, then about one month of testing and polish called the **release freeze**. +- *Starting with v1.26 the release cycle will be more predictable and follow a more regular schedule.* + +### Release schedule + +We will try to publish a new major version every three months: + +- v1.26.0 in April 2026 +- v1.27.0 in June 2026 +- v1.28.0 in September 2026 +- v1.29.0 in December 2026 + +#### How is the release handled? +- The release manager will tag the release candidate (e.g. `v1.26.0-rc0`) and publish it for testing in the **first week of the release month**. +- If there are no major issues, the release manager will check with the other maintainers and then tag the final release (e.g. `v1.26.0`) in the **one or two weeks following the release candidate**. + +### Feature freeze + +- Merge feature PRs before the freeze when you can. +- Feature PRs still open at the freeze move to the next milestone. Watch Discord for the freeze announcement. +- During the freeze, a **release branch** takes fixes backported from `main`. Release candidates ship for testing; the final release for that line is maintained from that branch. + +### Patch releases + +During a cycle we may ship patch releases for an older line. For example, if the latest release is v1.2, we can still publish v1.1.1 after v1.1.0. + +### End of life (EOL) + +We support per standard the last major release. For example, if the latest release is v1.26, we support v1.26 and v1.25, but not v1.24 anymore. We will only publish security fixes for the last major release, so if you are using an older release, please upgrade to a supported release as soon as possible. +Also we always try to support the latest on main branch, so if you are using the latest on main, you should be fine. + +## Versions + +Gitea has the `main` branch as a tip branch and has version branches +such as `release/v1.19`. `release/v1.19` is a release branch and we will +tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept +pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag, +after bringing the bug fix also to the main branch. + +Since the `main` branch is a tip version, if you wish to use Gitea +in production, please download the latest release tag version. All the +branches will be protected via GitHub, all the PRs to every branch must +be reviewed by two maintainers and must pass the automatic tests. + +## Releasing Gitea + +- Let MAJOR, MINOR and PATCH be Major, Minor and Patch version numbers, PATCH should be rc1, rc2, 0, 1, ...... MAJOR.MINOR will be kept the same as milestones on github or gitea in future. +- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours. +- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps: + - Create `-dev` tag as `git tag -s -F release.notes vMAJOR.MINOR.0-dev` and push the tag as `git push origin vMAJOR.MINOR.0-dev`. + - When CI has finished building tag then you have to create a new branch named `release/vMAJOR.MINOR` +- If it is bugfix version create PR for changelog on branch `release/vMAJOR.MINOR` and wait till it is reviewed and merged. +- Add a tag as `git tag -s -F release.notes vMAJOR.MINOR.PATCH`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`. +- And then push the tag as `git push origin vMAJOR.MINOR.$`. CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) +- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. +- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. +- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: + - bump the version of https://dl.gitea.com/gitea/version.json + - merge the blog post PR + - announce the release in discord `#announcements` diff --git a/flake.lock b/flake.lock index 25ce7939b35..8ec14d28526 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", "type": "github" }, "original": { diff --git a/models/issues/comment.go b/models/issues/comment.go index 34ce7f35004..84a7150b9f7 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/structs" @@ -543,6 +544,12 @@ func (c *Comment) EventTag() string { return fmt.Sprintf("event-%d", c.ID) } +func (c *Comment) GetSanitizedContentHTML() template.HTML { + // mainly for type=4 CommentTypeCommitRef + // the content is a link like message title (from CreateRefComment) + return markup.Sanitize(c.Content) +} + // LoadLabel if comment.Type is CommentTypeLabel, then load Label func (c *Comment) LoadLabel(ctx context.Context) error { var label Label diff --git a/modules/markup/html.go b/modules/markup/html.go index 1c2ae6918de..0fe37ae3052 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -6,6 +6,7 @@ package markup import ( "bytes" "fmt" + "html/template" "io" "regexp" "slices" @@ -149,9 +150,9 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e return postProcess(ctx, procs, input, output) } -// PostProcessCommitMessage will use the same logic as PostProcess, but will disable -// the shortLinkProcessor. -func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error) { +// PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor. +// FIXME: this function and its family have a very strange design: it takes HTML as input and output, processes the "escaped" content. +func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -165,7 +166,8 @@ func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error emojiProcessor, emojiShortCodeProcessor, } - return postProcessString(ctx, procs, content) + s, err := postProcessString(ctx, procs, string(content)) + return template.HTML(s), err } var emojiProcessors = []processor{ diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 2009be0bbd3..3c1ad144282 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -201,7 +201,7 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) { if HasInstallLock(rootCfg) { currentUser, match := IsRunUserMatchCurrentUser(RunUser) if !match { - log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser) + log.Fatal("Expect user '%s' (RUN_USER in app.ini) but current user is: %s", RunUser, currentUser) } } } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index f81be1255ab..3e4289c8ada 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -32,13 +32,12 @@ func newFuncMapWebPage() template.FuncMap { // ----------------------------------------------------------------- // html/template related functions - "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. - "Iif": iif, - "Eval": evalTokens, - "HTMLFormat": htmlFormat, - "QueryEscape": queryEscape, - "QueryBuild": QueryBuild, - "SanitizeHTML": SanitizeHTML, + "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. + "Iif": iif, + "Eval": evalTokens, + "HTMLFormat": htmlFormat, + "QueryEscape": queryEscape, + "QueryBuild": QueryBuild, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, @@ -146,9 +145,8 @@ func newFuncMapWebPage() template.FuncMap { } } -// SanitizeHTML sanitizes the input by default sanitization rules. -func SanitizeHTML(s string) template.HTML { - return markup.Sanitize(s) +func sanitizeHTML(msg string) template.HTML { + return markup.Sanitize(msg) } func htmlFormat(s any, args ...any) template.HTML { diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index f90818c0ad9..cf1db324768 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -58,7 +58,7 @@ func TestSubjectBodySeparator(t *testing.T) { } func TestSanitizeHTML(t *testing.T) { - assert.Equal(t, template.HTML(`link xss
inline
`), SanitizeHTML(`link xss
inline
`)) + assert.Equal(t, template.HTML(`link xss
inline
`), sanitizeHTML(`link xss
inline
`)) } func TestTemplateIif(t *testing.T) { diff --git a/modules/templates/mail.go b/modules/templates/mail.go index 181c6312b03..f81073902f3 100644 --- a/modules/templates/mail.go +++ b/modules/templates/mail.go @@ -65,13 +65,16 @@ func mailBodyFuncMap() template.FuncMap { "NIL": func() any { return nil }, // html/template related functions - "dict": dict, - "Iif": iif, - "Eval": evalTokens, - "HTMLFormat": htmlFormat, - "QueryEscape": queryEscape, - "QueryBuild": QueryBuild, - "SanitizeHTML": SanitizeHTML, + "dict": dict, + "Iif": iif, + "Eval": evalTokens, + "HTMLFormat": htmlFormat, + "QueryEscape": queryEscape, + "QueryBuild": QueryBuild, + + // deprecated, use "HTMLFormat" instead, but some user custom mail templates still use it + // see: https://github.com/go-gitea/gitea/issues/36049 + "SanitizeHTML": sanitizeHTML, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 081a13fb9ec..6641ee79594 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -40,7 +40,7 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { // RenderCommitMessage renders commit message with XSS-safe and special links. func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { - cleanMsg := template.HTMLEscapeString(msg) + cleanMsg := template.HTML(template.HTMLEscapeString(msg)) // we can safely assume that it will not return any error, since there shouldn't be any special HTML. // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) @@ -48,7 +48,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) te log.Error("PostProcessCommitMessage: %v", err) return "" } - msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") + msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n") if len(msgLines) == 0 { return "" } @@ -91,12 +91,14 @@ func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) templ return "" } - renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine)) + rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) + htmlContent := template.HTML(template.HTMLEscapeString(msgLine)) + renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" } - return template.HTML(renderedMessage) + return renderedMessage } // Match text that is between back ticks. @@ -279,6 +281,35 @@ func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize in return htmlutil.HTMLFormat(`
%s %s %s
`, info.GetDescription(), icon, info.DisplayName, extraIcon) } +func (ut *RenderUtils) RenderFlashMessage(typ, msg string) template.HTML { + msg = strings.TrimSpace(msg) + if msg == "" { + return "" + } + + cls := typ + // legacy logic: "negative" for error, "positive" for success + switch cls { + case "error": + cls = "negative" + case "success": + cls = "positive" + } + + var msgContent template.HTML + if strings.Contains(msg, "") || strings.Contains(msg, "") || strings.Contains(msg, "") || strings.Contains(msg, "") { + // If the message contains some known "block" elements, no need to do more alignment or line-break processing, just sanitize it directly. + msgContent = sanitizeHTML(msg) + } else if !strings.Contains(msg, "\n") { + // If the message is a single line, center-align it by wrapping it + msgContent = htmlutil.HTMLFormat(`
%s
`, sanitizeHTML(msg)) + } else { + // For a multi-line message, preserve line breaks, and left-align it. + msgContent = htmlutil.HTMLFormat(`%s`, sanitizeHTML(strings.ReplaceAll(msg, "\n", "
"))) + } + return htmlutil.HTMLFormat(`
%s
`, cls, typ, msgContent) +} + func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML { if escapeStatus == nil || !escapeStatus.Escaped { return "" diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 9d61e3f1d77..b9d5247b3d4 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -269,7 +269,7 @@ "install.lfs_path": "Git LFS Root Path", "install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.", "install.run_user": "Run As Username", - "install.run_user_helper": "The operating system username that Gitea runs as. Note that this user must have access to the repository root path.", + "install.run_user_helper": "The operating system username that Gitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart Gitea under that account.", "install.domain": "Server Domain", "install.domain_helper": "Domain or host address for the server.", "install.ssh_port": "SSH Server Port", @@ -316,7 +316,6 @@ "install.invalid_db_table": "The database table \"%s\" is invalid: %v", "install.invalid_repo_path": "The repository root path is invalid: %v", "install.invalid_app_data_path": "The app data path is invalid: %v", - "install.run_user_not_match": "The 'run as' username is not the current username: %s -> %s", "install.internal_token_failed": "Failed to generate internal token: %v", "install.secret_key_failed": "Failed to generate secret key: %v", "install.save_config_failed": "Failed to save configuration: %v", diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 6285138c27d..69dceb6db9d 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -62,13 +62,20 @@ func CompareDiff(ctx *context.APIContext) { apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits)) userCache := make(map[string]*user_model.User) + for i := 0; i < len(compareInfo.Commits); i++ { - apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo.Commits[i], userCache, + apiCommit, err := convert.ToCommit( + ctx, + compareInfo.HeadRepo, + compareInfo.HeadGitRepo, + compareInfo.Commits[i], + userCache, convert.ToCommitOptions{ Stat: true, Verification: verification, Files: files, - }) + }, + ) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/install/install.go b/routers/install/install.go index dec0b31e5cd..a0f32fb939c 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -26,7 +26,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/common" @@ -87,15 +86,7 @@ func Install(ctx *context.Context) { form.AppName = setting.AppName form.RepoRootPath = setting.RepoRootPath form.LFSRootPath = setting.LFS.Storage.Path - - // Note(unknown): it's hard for Windows users change a running user, - // so just use current one if config says default. - if setting.IsWindows && setting.RunUser == "git" { - form.RunUser = user.CurrentUsername() - } else { - form.RunUser = setting.RunUser - } - + form.RunUser = setting.RunUser form.Domain = setting.Domain form.SSHPort = setting.SSH.Port form.HTTPPort = setting.HTTPPort @@ -272,13 +263,6 @@ func SubmitInstall(ctx *context.Context) { return } - currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser) - if !match { - ctx.Data["Err_RunUser"] = true - ctx.RenderWithErrDeprecated(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form) - return - } - // Check logic loophole between disable self-registration and no admin account. if form.DisableRegistration && len(form.AdminName) == 0 { ctx.Data["Err_Services"] = true diff --git a/routers/utils/utils.go b/routers/utils/utils.go index 3035073d5c5..47ca13b2aa5 100644 --- a/routers/utils/utils.go +++ b/routers/utils/utils.go @@ -5,10 +5,11 @@ package utils import ( "html" - "strings" + "html/template" ) -// SanitizeFlashErrorString will sanitize a flash error string -func SanitizeFlashErrorString(x string) string { - return strings.ReplaceAll(html.EscapeString(x), "\n", "
") +// EscapeFlashErrorString will escape the flash error string +// Maybe do more sanitization in the future, e.g.: hide sensitive information, etc. +func EscapeFlashErrorString(x string) template.HTML { + return template.HTML(html.EscapeString(x)) } diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go index cc7c888a758..5dfc5447774 100644 --- a/routers/utils/utils_test.go +++ b/routers/utils/utils_test.go @@ -4,16 +4,17 @@ package utils import ( + "html/template" "testing" "github.com/stretchr/testify/assert" ) -func TestSanitizeFlashErrorString(t *testing.T) { +func TestEscapeFlashErrorString(t *testing.T) { tests := []struct { name string arg string - want string + want template.HTML }{ { name: "no error", @@ -28,13 +29,13 @@ func TestSanitizeFlashErrorString(t *testing.T) { { name: "line break error", arg: "some error:\n\nawesome!", - want: "some error:

awesome!", + want: "some error:\n\nawesome!", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := SanitizeFlashErrorString(tt.arg) + got := EscapeFlashErrorString(tt.arg) assert.Equal(t, tt.want, got) }) } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index c103d200645..8645aedbdee 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -79,7 +79,7 @@ func SignInOAuthCallback(ctx *context.Context) { } } sort.Strings(errorKeyValues) - ctx.Flash.Error(strings.Join(errorKeyValues, "
"), true) + ctx.Flash.Error(strings.Join(errorKeyValues, "\n"), true) } // first look if the provider is still active diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 5cfe08c7fe0..8bc5947df8f 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -45,8 +45,8 @@ func List(ctx *context.Context) { func FetchActionTest(ctx *context.Context) { _ = ctx.Req.ParseForm() - ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "
" + - "Form: " + ctx.Req.Form.Encode() + "
" + + ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" + + "Form: " + ctx.Req.Form.Encode() + "\n" + "PostForm: " + ctx.Req.PostForm.Encode(), ) time.Sleep(2 * time.Second) @@ -192,11 +192,31 @@ func prepareMockData(ctx *context.Context) { prepareMockDataBadgeActionsSvg(ctx) case "/devtest/relative-time": prepareMockDataRelativeTime(ctx) + case "/devtest/toast-and-message": + prepareMockDataToastAndMessage(ctx) case "/devtest/unicode-escape": prepareMockDataUnicodeEscape(ctx) } } +func prepareMockDataToastAndMessage(ctx *context.Context) { + msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with details ", + "Summary": "summary with details", + "Details": "details line 1\n details line 2\n details line 3", + }) + msgWithSummary, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with summary ", + "Summary": "summary only", + }) + + ctx.Flash.ErrorMsg = string(msgWithDetails) + ctx.Flash.WarningMsg = string(msgWithSummary) + ctx.Flash.InfoMsg = "a long message with line break\nthe second line " + ctx.Flash.SuccessMsg = "single line message " + ctx.Data["Flash"] = ctx.Flash +} + func prepareMockDataUnicodeEscape(ctx *context.Context) { content := "// demo code\n" content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n" @@ -223,8 +243,8 @@ func TmplCommon(ctx *context.Context) { prepareMockData(ctx) if ctx.Req.Method == http.MethodPost { _ = ctx.Req.ParseForm() - ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"
"+ - "Form: "+ctx.Req.Form.Encode()+"
"+ + ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+ + "Form: "+ctx.Req.Form.Encode()+"\n"+ "PostForm: "+ctx.Req.PostForm.Encode(), true, ) diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index a5c379e01a9..5d208bb2861 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -15,6 +15,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -237,7 +238,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio } } if len(content) == 0 { - content = templates.SanitizeHTML(desc) + content = markup.Sanitize(desc) } items = append(items, &feeds.Item{ diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5e5cfec5c2b..c566e465e9d 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -231,7 +231,7 @@ func CreateBranch(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(e.Message), + "Details": utils.EscapeFlashErrorString(e.Message), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 168d9594940..34e588b1416 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -410,7 +410,8 @@ func Diff(ctx *context.Context) { ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))}) - ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, htmlMessage) if err != nil { ctx.ServerError("PostProcessCommitMessage", err) return diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go index e1473a34b39..f23b2738e5e 100644 --- a/routers/web/repo/editor_error.go +++ b/routers/web/repo/editor_error.go @@ -27,13 +27,13 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": message, "Summary": summary, - "Details": utils.SanitizeFlashErrorString(details), + "Details": utils.EscapeFlashErrorString(details), }) if err == nil { ctx.JSONError(flashError) } else { - log.Error("RenderToHTML: %v", err) - ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + log.Error("RenderToHTML(%q, %q, %q), error: %v", message, summary, details, err) + ctx.JSONError("Unable to render error details, see server logs") // it should never happen } } diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 98fb842ddf7..592d902ba8e 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -170,7 +170,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) templat flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), - "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")), + "Details": utils.EscapeFlashErrorString(strings.Join(lines, "\n")), }) if err != nil { log.Debug("render flash error: %v", err) diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 250a54fc24a..f678f838784 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -29,7 +29,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" @@ -781,14 +780,14 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue } else if comment.Type == issues_model.CommentTypeAddTimeManual || comment.Type == issues_model.CommentTypeStopTracking || comment.Type == issues_model.CommentTypeDeleteTimeManual { - // drop error since times could be pruned from DB.. + // drop error since times could be pruned from DB _ = comment.LoadTime(ctx) if comment.Content != "" { // Content before v1.21 did store the formatted string instead of seconds, // so "|" is used as delimiter to mark the new format if comment.Content[0] != '|' { // handle old time comments that have formatted text stored - comment.RenderedContent = templates.SanitizeHTML(comment.Content) + comment.RenderedContent = markup.Sanitize(comment.Content) comment.Content = "" } else { // else it's just a duration in seconds to pass on to the frontend diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index e312fc9d2a8..efcdaac6740 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1042,7 +1042,7 @@ func UpdatePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.merge_conflict"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) @@ -1054,9 +1054,9 @@ func UpdatePullRequest(ctx *context.Context) { } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) @@ -1191,7 +1191,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.merge_conflict"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1202,9 +1202,9 @@ func MergePullRequest(ctx *context.Context) { } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1234,7 +1234,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1454,7 +1454,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("CompareAndPullRequest.HTMLString", err) diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl index 5ebe1917712..242f6278ea7 100644 --- a/templates/base/alert.tmpl +++ b/templates/base/alert.tmpl @@ -1,25 +1,9 @@ -{{- if .Flash.ErrorMsg -}} -
-

{{.Flash.ErrorMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.SuccessMsg -}} -
-

{{.Flash.SuccessMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.InfoMsg -}} -
-

{{.Flash.InfoMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.WarningMsg -}} -
-

{{.Flash.WarningMsg | SanitizeHTML}}

-
-{{- end -}} +{{- if .Flash.ErrorMsg}}{{ctx.RenderUtils.RenderFlashMessage "error" .Flash.ErrorMsg}}{{end -}} +{{- if .Flash.WarningMsg}}{{ctx.RenderUtils.RenderFlashMessage "warning" .Flash.WarningMsg}}{{end -}} +{{- if .Flash.InfoMsg}}{{ctx.RenderUtils.RenderFlashMessage "info" .Flash.InfoMsg}}{{end -}} +{{- if .Flash.SuccessMsg}}{{ctx.RenderUtils.RenderFlashMessage "success" .Flash.SuccessMsg}}{{end -}} {{- if .ShowTwoFactorRequiredMessage -}} -
-

{{ctx.Locale.Tr "auth.twofa_required"}}

+ {{- end -}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 6380a72498c..da8aba452a6 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -2,10 +2,8 @@ {{if .Details}}
{{.Summary}} - {{.Details | SanitizeHTML}} +
{{.Details}}
{{else}} -
- {{.Summary}} -
+
{{.Summary}}
{{end}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 628e4388a0c..c9d7b3047fe 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,4 +1,4 @@ {{template "base/head" ctx.RootData}}
-{{template "base/alert" .}} +
{{template "base/alert" ctx.RootData}}
diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl index 4ee824f04be..cd4da52aac3 100644 --- a/templates/devtest/fetch-action.tmpl +++ b/templates/devtest/fetch-action.tmpl @@ -1,6 +1,5 @@ {{template "devtest/devtest-header"}}
- {{template "base/alert" .}}

link-action

@@ -17,29 +16,20 @@

form-fetch-action

Use "window.fetch" to send a form request to backend
-
-
+
+ -
+
-
+
bad action url
- {{template "devtest/devtest-footer"}} diff --git a/templates/devtest/form-fields.tmpl b/templates/devtest/form-fields.tmpl new file mode 100644 index 00000000000..ee6df2e813f --- /dev/null +++ b/templates/devtest/form-fields.tmpl @@ -0,0 +1,109 @@ +{{template "devtest/devtest-header"}} +
+
+

Input

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Textarea

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Dropdown

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Required

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+{{template "devtest/devtest-footer"}} diff --git a/templates/devtest/toast.tmpl b/templates/devtest/toast-and-message.tmpl similarity index 96% rename from templates/devtest/toast.tmpl rename to templates/devtest/toast-and-message.tmpl index 597b4154695..c4056b6fc6b 100644 --- a/templates/devtest/toast.tmpl +++ b/templates/devtest/toast-and-message.tmpl @@ -1,5 +1,5 @@ {{template "devtest/devtest-header"}} -
+

Toast

diff --git a/templates/install.tmpl b/templates/install.tmpl index 45f14d5c575..bc6fed08e95 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -117,7 +117,7 @@ {{ctx.Locale.Tr "install.lfs_path_helper"}}
-
+
{{ctx.Locale.Tr "install.run_user_helper"}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 179f6018d3d..8451a6f3cf1 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -195,7 +195,7 @@ {{DateUtils.TimeSince .NoteCommit.Author.When}}
-
{{.NoteRendered | SanitizeHTML}}
+
{{.NoteRendered}}
{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 8c08f1c1f1c..6a67ef754d7 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -164,7 +164,7 @@
{{svg "octicon-git-commit"}} {{/* the content is a link like message title (from CreateRefComment) */}} - {{.Content | SanitizeHTML}} + {{.GetSanitizedContentHTML}}
{{else if eq .Type 7}} diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index 9565e4d2090..8aa0035b0a9 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -5,46 +5,60 @@ package integration import ( "net/http" + "net/url" "testing" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPICompareBranches(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + session2 := loginUser(t, "user2") + token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - // Login as User2. - session := loginUser(t, user.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + t.Run("CompareBranches", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("CompareBranches", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token2) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 2, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 2) + }) - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + t.Run("CompareCommits", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - assert.Equal(t, 2, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 2) - }) + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token2) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 1, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 1) + }) - t.Run("CompareCommits", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + t.Run("CompareForkOnlyCommit", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) + repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) + user13Sess := loginUser(t, "user13") + user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository) - assert.Equal(t, 1, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 1) + _, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "new-branch"}, map[string]string{"file.txt": "content"}) + require.NoError(t, err) + req := NewRequestf(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:new-branch").AddTokenAuth(user13Token) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 1, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 1) + }) }) } diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 53709e6ff4b..d0213962ee7 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -317,9 +317,8 @@ func TestPullCleanUpAfterMerge(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - resultMsg := htmlDoc.doc.Find(".ui.message>p").Text() - - assert.Equal(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg) + resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text()) + assert.Equal(t, `Branch "user1/repo1:feature/test" has been deleted.`, resultMsg) }) } diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index ff35baae9db..4000c7ebe1d 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -36,8 +36,7 @@ func testLoginFailed(t *testing.T, username, password, message string) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - resultMsg := htmlDoc.doc.Find(".ui.message>p").Text() - + resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text()) assert.Equal(t, message, resultMsg) } diff --git a/tests/integration/user_settings_test.go b/tests/integration/user_settings_test.go index 20c758dc85a..b3527dd467a 100644 --- a/tests/integration/user_settings_test.go +++ b/tests/integration/user_settings_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "strings" "testing" "code.gitea.io/gitea/modules/container" @@ -309,8 +310,7 @@ func TestUserSettingsApplications(t *testing.T) { }) resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - - msg := doc.Find(".flash-error p").Text() + msg := strings.TrimSpace(doc.Find(".ui.message.flash-message").Text()) assert.Equal(t, `form.RedirectURIs"ftp://127.0.0.1" is not a valid URL.`, msg) }) diff --git a/web_src/css/actions.css b/web_src/css/actions.css index c43ebe21a05..14cf65f273d 100644 --- a/web_src/css/actions.css +++ b/web_src/css/actions.css @@ -6,14 +6,6 @@ overflow-x: auto; } -.runner-container .runner-new-text { - color: var(--color-white); -} - -.runner-container #runner-new:hover .runner-new-text { - color: var(--color-white) !important; -} - .runner-container .task-status-success { background-color: var(--color-green); color: var(--color-white); diff --git a/web_src/css/base.css b/web_src/css/base.css index a8d9dea2a25..4448af68e0c 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -431,8 +431,9 @@ img.ui.avatar, margin-top: calc(var(--page-spacing) - 1rem); } -.ui .message.flash-message { - text-align: center; +.ui.message.flash-message pre { + white-space: pre-line; + margin: 0; } .ui .header > i + .content { @@ -643,10 +644,6 @@ overflow-menu .ui.label { color: var(--color-primary-contrast); } -.archived-icon { - color: var(--color-secondary-dark-2) !important; -} - .oauth2-authorize-application-box { margin-top: 3em !important; } @@ -670,10 +667,6 @@ overflow-menu .ui.label { min-width: 50px; } -.lines-num span.bottom-line::after { - border-bottom: 1px solid var(--color-secondary); -} - .lines-num span::after { content: attr(data-line-number); line-height: var(--line-height-code) !important; @@ -783,11 +776,6 @@ tr.top-line-blame:first-of-type { border-top: none; /* merge code lines belonging to the same commit into one block */ } -.lines-code .bottom-line, -.lines-commit .bottom-line { - border-bottom: 1px solid var(--color-secondary); -} - .migrate .svg.gitea-git { color: var(--color-git); } @@ -878,6 +866,13 @@ table th[data-sortt-desc] .svg { align-items: stretch; } +/* can be used to replace "ui relaxed list" or "tw-flex tw-flex-col tw-gap-xxx" when we need more flexible layout */ +.flex-relaxed-list { + display: flex; + flex-direction: column; + gap: var(--gap-block); +} + .ui.list.flex-items-block > .item, .ui.vertical.menu.flex-items-block > .item, .ui.form .field > label.flex-text-block, /* override fomantic "block" style */ diff --git a/web_src/css/devtest.css b/web_src/css/devtest.css index a7b00e1e561..c344d99058b 100644 --- a/web_src/css/devtest.css +++ b/web_src/css/devtest.css @@ -1,3 +1,8 @@ +h1, h2 { + margin: 0; + padding: 10px 0; +} + .button-sample-groups { margin: 0; padding: 0; } @@ -10,7 +15,6 @@ margin-bottom: 5px; } -h1, h2 { - margin: 0; - padding: 10px 0; +.fetch-action-demo-forms .form-fetch-action { + border: 1px red dashed; /* show the border for demo purpose */ } diff --git a/web_src/css/markup/asciicast.css b/web_src/css/markup/asciicast.css index 89696bc7105..a45daaa8e8b 100644 --- a/web_src/css/markup/asciicast.css +++ b/web_src/css/markup/asciicast.css @@ -3,6 +3,8 @@ height: auto; } -.ap-terminal { +/* Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js :
+Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159 */ +.ap-term { overflow: hidden !important; } diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index e7a967a7c64..efa6947ef14 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -154,12 +154,6 @@ In markup content, we always use bottom margin for all elements */ padding-inline-start: 2em; } -.markup ul.no-list, -.markup ol.no-list { - padding: 0; - list-style-type: none; -} - .markup .task-list-item { list-style-type: none; } @@ -357,69 +351,6 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { color: var(--color-text); } -.markup span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markup span.align-center > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markup span.align-center span img, -.markup span.align-center span video { - margin: 0 auto; - text-align: center; -} - -.markup span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markup span.align-right > span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markup span.align-right span img, -.markup span.align-right span video { - margin: 0; - text-align: right; -} - -.markup span.float-left { - display: block; - float: left; - margin-inline-end: 13px; - overflow: hidden; -} - -.markup span.float-left span { - margin: 13px 0 0; -} - -.markup span.float-right { - display: block; - float: right; - margin-inline-start: 13px; - overflow: hidden; -} - -.markup span.float-right > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - .markup code, .markup tt { padding: 0.2em 0.4em; diff --git a/web_src/css/modules/form.css b/web_src/css/modules/form.css index 2d315786c6f..2999f64cf6b 100644 --- a/web_src/css/modules/form.css +++ b/web_src/css/modules/form.css @@ -99,6 +99,13 @@ textarea:focus, color: var(--color-input-text); } +.ui.form input:not([type="checkbox"], [type="radio"])[readonly], +.ui.form textarea[readonly], +.ui.form select[readonly], +.ui.form .ui.selection.dropdown[readonly] { + background: var(--color-secondary-bg); +} + .ui.input { color: var(--color-input-text); } @@ -198,7 +205,6 @@ textarea:focus, background-color: var(--color-error-bg); border-color: var(--color-error-border); color: var(--color-error-text); - border-radius: 0; } .ui.form .field.error textarea:focus, .ui.form .field.error select:focus, diff --git a/web_src/css/modules/modal.css b/web_src/css/modules/modal.css index 5d686746cbe..d45e54b947b 100644 --- a/web_src/css/modules/modal.css +++ b/web_src/css/modules/modal.css @@ -159,19 +159,11 @@ display: block; } -.scrolling.dimmable.dimmed { - overflow: hidden; -} - .scrolling.dimmable > .dimmer { justify-content: flex-start; position: fixed; } -.scrolling.dimmable.dimmed > .dimmer { - overflow: auto; -} - .modals.dimmer .ui.scrolling.modal { margin: 2rem auto; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 95d6ca21695..923aef04ba4 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -287,10 +287,6 @@ td .commit-summary { min-width: 100px; } -.repository.view.issue .instruct-toggle { - display: inline-block; -} - /* issue title & meta & edit */ .issue-title-header { width: 100%; @@ -1463,11 +1459,6 @@ tbody.commit-list { } } -.commit-list .commit-status-link { - display: inline-block; - vertical-align: middle; -} - .commit-body { margin: 0.25em 0; white-space: pre-wrap;