diff --git a/.air.toml b/.air.toml index 0610088303..069a889243 100644 --- a/.air.toml +++ b/.air.toml @@ -5,6 +5,6 @@ tmp_dir = ".air" cmd = "make backend" bin = "gitea" include_ext = ["go", "tmpl"] -exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata"] -include_dir = ["cmd", "models", "modules", "options", "routers", "services", "templates"] +exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"] +include_dir = ["cmd", "models", "modules", "options", "routers", "services"] exclude_regex = ["_test.go$", "_gen.go$"] diff --git a/.drone.yml b/.drone.yml index 8a73e84a00..072a43cf9a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -769,10 +769,16 @@ steps: image: woodpeckerci/plugin-s3:latest pull: always settings: - acl: public-read - bucket: gitea-artifacts - endpoint: https://ams3.digitaloceanspaces.com - path_style: true + acl: + from_secret: aws_s3_acl + region: + from_secret: aws_s3_region + bucket: + from_secret: aws_s3_bucket + endpoint: + from_secret: aws_s3_endpoint + path_style: + from_secret: aws_s3_path_style source: "dist/release/*" strip_prefix: dist/release/ target: "/gitea/${DRONE_BRANCH##release/v}" @@ -790,10 +796,16 @@ steps: - name: release-main image: woodpeckerci/plugin-s3:latest settings: - acl: public-read - bucket: gitea-artifacts - endpoint: https://ams3.digitaloceanspaces.com - path_style: true + acl: + from_secret: aws_s3_acl + region: + from_secret: aws_s3_region + bucket: + from_secret: aws_s3_bucket + endpoint: + from_secret: aws_s3_endpoint + path_style: + from_secret: aws_s3_path_style source: "dist/release/*" strip_prefix: dist/release/ target: /gitea/main @@ -892,10 +904,16 @@ steps: image: woodpeckerci/plugin-s3:latest pull: always settings: - acl: public-read - bucket: gitea-artifacts - endpoint: https://ams3.digitaloceanspaces.com - path_style: true + acl: + from_secret: aws_s3_acl + region: + from_secret: aws_s3_region + bucket: + from_secret: aws_s3_bucket + endpoint: + from_secret: aws_s3_endpoint + path_style: + from_secret: aws_s3_path_style source: "dist/release/*" strip_prefix: dist/release/ target: "/gitea/${DRONE_TAG##v}" @@ -941,7 +959,8 @@ steps: image: plugins/hugo:latest pull: always commands: - - apk add --no-cache make bash curl + # https://github.com/drone-plugins/drone-hugo/issues/36 + - apk upgrade --no-cache libcurl && apk add --no-cache make bash curl - cd docs - make trans-copy clean build diff --git a/.gitea/ISSUE_TEMPLATE/bug-report.md b/.gitea/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000000..138df035b9 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,53 @@ +--- +name: "Bug Report" +about: "Found something you weren't expecting? Report it here!" +title: "[BUG] " +--- + + +- Forgejo version (or commit ref): +- Git version: +- Operating system: +- Database (use `[x]`): + - [ ] PostgreSQL + - [ ] MySQL + - [ ] MSSQL + - [ ] SQLite +- How are you running Forgejo? + + +## Description + + +## Logs + + +## Screenshots + diff --git a/.gitea/ISSUE_TEMPLATE/feature-request.md b/.gitea/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000000..3708f2514e --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,24 @@ +--- +name: "Feature Request" +about: "Got an idea for a feature that Forgejo doesn't have yet? Submit it here!" +title: "[FEAT] " +--- + + +## Needs and benefits + + +## Feature Description + + +## Screenshots + diff --git a/.gitea/issue_template.md b/.gitea/issue_template.md deleted file mode 100644 index 95b97e4de5..0000000000 --- a/.gitea/issue_template.md +++ /dev/null @@ -1,42 +0,0 @@ - - - - -- Gitea version (or commit ref): -- Git version: -- Operating system: - - - -- Database (use `[x]`): - - [ ] PostgreSQL - - [ ] MySQL - - [ ] MSSQL - - [ ] SQLite -- Can you reproduce the bug at https://try.gitea.io: - - [ ] Yes (provide example URL) - - [ ] No -- Log gist: - - - - -## Description - - -... - - -## Screenshots - - diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000000..a94ec46201 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,4 @@ + diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 624a2d97db..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -open_collective: gitea -custom: https://www.bountysource.com/teams/gitea diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml deleted file mode 100644 index 9dacad0d5f..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ /dev/null @@ -1,94 +0,0 @@ -name: Bug Report -description: Found something you weren't expecting? Report it here! -labels: kind/bug -body: -- type: markdown - attributes: - value: | - NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. -- type: markdown - attributes: - value: | - 1. Please speak English, this is the language all maintainers can speak and write. - 2. Please ask questions or configuration/deploy problems on our Discord - server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). - 3. Make sure you are using the latest release and - take a moment to check that your issue hasn't been reported before. - 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.io/en-us/faq) - 5. Please give all relevant information below for bug reports, because - incomplete details will be handled as an invalid report. - 6. In particular it's really important to provide pertinent logs. You must give us DEBUG level logs. - Please read https://docs.gitea.io/en-us/logging-configuration/#debugging-problems - In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini -- type: textarea - id: description - attributes: - label: Description - description: | - Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) - If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services. -- type: input - id: gitea-ver - attributes: - label: Gitea Version - description: Gitea version (or commit reference) of your instance - validations: - required: true -- type: dropdown - id: can-reproduce - attributes: - label: Can you reproduce the bug on the Gitea demo site? - description: | - If so, please provide a URL in the Description field - URL of Gitea demo: https://try.gitea.io - options: - - "Yes" - - "No" - validations: - required: true -- type: markdown - attributes: - value: | - It's really important to provide pertinent logs - Please read https://docs.gitea.io/en-us/logging-configuration/#debugging-problems - In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini -- type: input - id: logs - attributes: - label: Log Gist - description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: If this issue involves the Web Interface, please provide one or more screenshots -- type: input - id: git-ver - attributes: - label: Git Version - description: The version of git running on the server -- type: input - id: os-ver - attributes: - label: Operating System - description: The operating system you are using to run Gitea -- type: textarea - id: run-info - attributes: - label: How are you running Gitea? - description: | - Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package - Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc. - If you are using a package or systemd tell us what distribution you are using - validations: - required: true -- type: dropdown - id: database - attributes: - label: Database - description: What database system are you running? - options: - - PostgreSQL - - MySQL - - MSSQL - - SQLite diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 9bb5bb8e88..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Security Concern - url: https://tinyurl.com/security-gitea - about: For security concerns, please send a mail to security@gitea.io instead of opening a public issue. - - name: Discord Server - url: https://discord.gg/Gitea - about: Please ask questions and discuss configuration or deployment problems here. - - name: Discourse Forum - url: https://discourse.gitea.io - about: Questions and configuration or deployment problems can also be discussed on our forum. - - name: Frequently Asked Questions - url: https://docs.gitea.io/en-us/faq - about: Please check if your question isn't mentioned here. - - name: Crowdin Translations - url: https://crowdin.com/project/gitea - about: Translations are managed here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml deleted file mode 100644 index 37f57c8f23..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: Feature Request -description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here! -labels: ["kind/feature", "kind/proposal"] -body: -- type: markdown - attributes: - value: | - 1. Please speak English, this is the language all maintainers can speak and write. - 2. Please ask questions or configuration/deploy problems on our Discord - server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). - 3. Please take a moment to check that your feature hasn't already been suggested. -- type: textarea - id: description - attributes: - label: Feature Description - placeholder: | - I think it would be great if Gitea had... - validations: - required: true -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: If you can, provide screenshots of an implementation on another site e.g. GitHub diff --git a/.github/ISSUE_TEMPLATE/ui.bug-report.yaml b/.github/ISSUE_TEMPLATE/ui.bug-report.yaml deleted file mode 100644 index 80db52d7f1..0000000000 --- a/.github/ISSUE_TEMPLATE/ui.bug-report.yaml +++ /dev/null @@ -1,66 +0,0 @@ -name: Web Interface Bug Report -description: Something doesn't look quite as it should? Report it here! -labels: ["kind/bug", "kind/ui"] -body: -- type: markdown - attributes: - value: | - NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. -- type: markdown - attributes: - value: | - 1. Please speak English, this is the language all maintainers can speak and write. - 2. Please ask questions or configuration/deploy problems on our Discord - server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). - 3. Please take a moment to check that your issue doesn't already exist. - 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.io/en-us/faq) - 5. Please give all relevant information below for bug reports, because - incomplete details will be handled as an invalid report. - 6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript - error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us - DEBUG level logs. (See https://docs.gitea.io/en-us/logging-configuration/#debugging-problems) -- type: textarea - id: description - attributes: - label: Description - description: | - Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) - If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services. -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: Please provide at least 1 screenshot showing the issue. - validations: - required: true -- type: input - id: gitea-ver - attributes: - label: Gitea Version - description: Gitea version (or commit reference) your instance is running - validations: - required: true -- type: dropdown - id: can-reproduce - attributes: - label: Can you reproduce the bug on the Gitea demo site? - description: | - If so, please provide a URL in the Description field - URL of Gitea demo: https://try.gitea.io - options: - - "Yes" - - "No" - validations: - required: true -- type: input - id: os-ver - attributes: - label: Operating System - description: The operating system you are using to access Gitea -- type: input - id: browser-ver - attributes: - label: Browser Version - description: The browser and version that you are using to access Gitea - validations: - required: true diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 6beadcaf11..0000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 60 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). `false` is disabled -skipCreatedBefore: false - -# Issues and pull requests with these labels will be ignored. -exemptLabels: [] - -# Label to add before locking, such as `outdated`. `false` is disabled -lockLabel: false - -# Comment to post before locking. -lockComment: > - This thread has been automatically locked since there has not been - any recent activity after it was closed. Please open a new issue for - related bugs and link to relevant comments in this thread. - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 3a12bb8f72..0000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 6a9f341cbf..0000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,54 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale - -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 - -# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 14 - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - status/blocked - - kind/security - - lgtm/done - - reviewed/confirmed - - priority/critical - - kind/proposal - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: false - -# Label to use when marking as stale -staleLabel: stale - -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had recent activity. - I am here to help clear issues left open even if solved or waiting for more insight. - This issue will be closed if no further activity occurs during the next 2 weeks. - If the issue is still valid just add a comment to keep it alive. - Thank you for your contributions. - -# Comment to post when closing a stale Issue or Pull Request. -closeComment: > - This issue has been automatically closed because of inactivity. - You can re-open it if needed. - -# Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 1 - -# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': -pulls: - daysUntilStale: 60 - daysUntilClose: 60 - markComment: > - This pull request has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs during the next 2 months. Thank you - for your contributions. - closeComment: > - This pull request has been automatically closed because of inactivity. - You can re-open it if needed. diff --git a/.gitignore b/.gitignore index 1ce2a87611..6ec3c3faed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Emacs +*~ + # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a @@ -112,3 +115,6 @@ prime/ # Manpage /man + +# Generated merged Forgejo+Gitea language files +/options/locale/locale_* diff --git a/.golangci.yml b/.golangci.yml index 99133badd9..d252b20235 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -173,3 +173,6 @@ issues: linters: - revive text: "exported: type name will be used as user.UserBadge by other packages, and that stutters; consider calling this Badge" + - path: models/db/sql_postgres_with_schema.go + linters: + - nolintlint diff --git a/.woodpecker/compliance.yml b/.woodpecker/compliance.yml new file mode 100644 index 0000000000..4a84dd8a62 --- /dev/null +++ b/.woodpecker/compliance.yml @@ -0,0 +1,75 @@ +platform: linux/amd64 + +when: + event: [ push, pull_request, manual ] + branch: + exclude: [ soft-fork/*/*, soft-fork/*/*/* ] + +variables: + - &golang_image 'golang:1.20' + - &test_image 'codeberg.org/forgejo/test_env:1.18' + - &goproxy_override '' + - &goproxy_setup |- + if [ -n "$${GOPROXY_OVERRIDE:-}" ]; then + export GOPROXY="$${GOPROXY_OVERRIDE}"; + echo "Using goproxy from goproxy_override \"$${GOPROXY}\""; + elif [ -n "$${GOPROXY_DEFAULT:-}" ]; then + export GOPROXY="$${GOPROXY_DEFAULT}"; + echo "Using goproxy from goproxy_default (secret) not displaying"; + else + export GOPROXY="https://proxy.golang.org,direct"; + echo "No goproxy overrides or defaults given, using \"$${GOPROXY}\""; + fi + +workspace: + base: /go + path: src/codeberg/gitea + +pipeline: + deps-backend: + image: *golang_image + pull: true + environment: + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + commands: + - *goproxy_setup + - make deps-backend + + security-check: + image: *golang_image + group: checks + pull: true + environment: + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + commands: + - *goproxy_setup + - make security-check + + lint-backend: + image: *test_image + pull: true + group: checks + environment: + GOPROXY_OVERRIDE: *goproxy_override + TAGS: 'bindata sqlite sqlite_unlock_notify' + GOSUMDB: 'sum.golang.org' + secrets: + - goproxy_default + commands: + - *goproxy_setup + - make lint-backend + + checks-backend: + image: *test_image + group: checks + environment: + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + commands: + - *goproxy_setup + - make --always-make checks-backend diff --git a/.woodpecker/testing-amd64.yml b/.woodpecker/testing-amd64.yml new file mode 100644 index 0000000000..c321a4bd78 --- /dev/null +++ b/.woodpecker/testing-amd64.yml @@ -0,0 +1,149 @@ +platform: linux/amd64 + +when: + event: [ push, pull_request, manual ] + branch: + exclude: [ soft-fork/*/*, soft-fork/*/*/* ] + +depends_on: +- compliance + +variables: + - &golang_image 'golang:1.20' + - &test_image 'codeberg.org/forgejo/test_env:1.18' + - &mysql_image 'mysql:8' + - &pgsql_image 'postgres:10' + - &goproxy_override '' + - &goproxy_setup |- + if [ -n "$${GOPROXY_OVERRIDE:-}" ]; then + export GOPROXY="$${GOPROXY_OVERRIDE}"; + echo "Using goproxy from goproxy_override \"$${GOPROXY}\""; + elif [ -n "$${GOPROXY_DEFAULT:-}" ]; then + export GOPROXY="$${GOPROXY_DEFAULT}"; + echo "Using goproxy from goproxy_default (secret) not displaying"; + else + export GOPROXY="https://proxy.golang.org,direct"; + echo "No goproxy overrides or defaults given, using \"$${GOPROXY}\""; + fi + +services: + mysql8: + image: *mysql_image + pull: true + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: testgitea + + pgsql: + image: *pgsql_image + pull: true + environment: + POSTGRES_DB: test + POSTGRES_PASSWORD: postgres + +workspace: + base: /go + path: src/codeberg/gitea + +pipeline: + git-safe: + image: *golang_image + pull: true + commands: + - git config --add safe.directory '*' + + deps-backend: + image: *golang_image + pull: true + environment: + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + commands: + - *goproxy_setup + - make deps-backend + + tag-pre-condition: + image: *golang_image + pull: true + commands: + - git update-ref refs/heads/tag_test ${CI_COMMIT_SHA} + + prepare-test-env: + image: *test_image + pull: true + commands: + - ./build/test-env-prepare.sh + + environment-to-ini: + image: *golang_image + environment: + GOPROXY_OVERRIDE: *goproxy_override + commands: + - *goproxy_setup + - go test contrib/environment-to-ini/environment-to-ini.go contrib/environment-to-ini/environment-to-ini_test.go + + build: + image: *test_image + environment: + GOSUMDB: sum.golang.org + TAGS: bindata sqlite sqlite_unlock_notify + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + commands: + - *goproxy_setup + - su gitea -c './build/test-env-check.sh' + - su gitea -c 'make backend' + + unit-test: + image: *test_image + environment: + TAGS: 'bindata sqlite sqlite_unlock_notify' + RACE_ENABLED: 'true' + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - github_read_token + - goproxy_default + commands: + - *goproxy_setup + - su gitea -c 'make unit-test-coverage test-check' + + test-mysql8: + group: integration + image: *test_image + commands: + - *goproxy_setup + - su gitea -c 'timeout -s ABRT 50m make test-mysql8-migration test-mysql8' + environment: + TAGS: 'bindata' + RACE_ENABLED: 'true' + USE_REPO_TEST_DIR: '1' + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + + test-pgsql: + group: integration + image: *test_image + commands: + - *goproxy_setup + - su gitea -c 'timeout -s ABRT 50m make test-pgsql-migration test-pgsql' + environment: + TAGS: 'bindata' + RACE_ENABLED: 'true' + USE_REPO_TEST_DIR: '1' + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + + test-sqlite: + group: integration + image: *test_image + environment: + - USE_REPO_TEST_DIR=1 + - GOPROXY=off + - TAGS=bindata gogit sqlite sqlite_unlock_notify + - TEST_TAGS=bindata gogit sqlite sqlite_unlock_notify + commands: + - su gitea -c 'timeout -s ABRT 120m make test-sqlite-migration test-sqlite' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1150d70081..358a0e3749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,367 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.18.5](https://github.com/go-gitea/gitea/releases/tag/v1.18.5) - 2023-02-21 + +* ENHANCEMENTS + * Hide 2FA status from other members in organization members list (#22999) (#23023) +* BUGFIXES + * Add force_merge to merge request and fix checking mergable (#23010) (#23032) + * Use `--message=%s` for git commit message (#23028) (#23029) + * Render access log template as text instead of HTML (#23013) (#23025) + * Fix the Manually Merged form (#23015) (#23017) + * Use beforeCommit instead of baseCommit (#22949) (#22996) + * Display attachments of review comment when comment content is blank (#23035) (#23046) + * Return empty url for submodule tree entries (#23043) (#23048) + +## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20 + +* SECURITY + * Provide the ability to set password hash algorithm parameters (#22942) (#22943) + * Add command to bulk set must-change-password (#22823) (#22928) +* ENHANCEMENTS + * Use import of OCI structs (#22765) (#22805) + * Fix color of tertiary button on dark theme (#22739) (#22744) + * Link issue and pull requests status change in UI notifications directly to their event in the timelined view. (#22627) (#22642) +* BUGFIXES + * Notify on container image create (#22806) (#22965) + * Fix blame view missing lines (#22826) (#22929) + * Fix incorrect role labels for migrated issues and comments (#22914) (#22923) + * Fix PR file tree folders no longer collapsing (#22864) (#22872) + * Escape filename when assemble URL (#22850) (#22871) + * Fix isAllowed of escapeStreamer (#22814) (#22837) + * Load issue before accessing index in merge message (#22822) (#22830) + * Improve trace logging for pulls and processes (#22633) (#22812) + * Fix restore repo bug, clarify the problem of ForeignIndex (#22776) (#22794) + * Add default user visibility to cli command "admin user create" (#22750) (#22760) + * Escape path for the file list (#22741) (#22757) + * Fix bugs with WebAuthn preventing sign in and registration. (#22651) (#22721) + * Add missing close bracket in imagediff (#22710) (#22712) + * Move code comments to a standalone file and fix the bug when adding a reply to an outdated review appears to not post(#20821) (#22707) + * Fix line spacing for plaintext previews (#22699) (#22701) + * Fix wrong hint when deleting a branch successfully from pull request UI (#22673) (#22698) + * Fix README TOC links (#22577) (#22677) + * Fix missing message in git hook when pull requests disabled on fork (#22625) (#22658) + * Improve checkIfPRContentChanged (#22611) (#22644) + * Prevent duplicate labels when importing more than 99 (#22591) (#22598) + * Don't return duplicated users who can create org repo (#22560) (#22562) +* BUILD + * Upgrade golangcilint to v1.51.0 (#22764) +* MISC + * Use proxy for pull mirror (#22771) (#22772) + * Use `--index-url` in PyPi description (#22620) (#22636) + +## [1.18.3](https://github.com/go-gitea/gitea/releases/tag/v1.18.3) - 2023-01-23 + +* SECURITY + * Prevent multiple `To` recipients (#22566) (#22569) +* BUGFIXES + * Truncate commit summary on repo files table. (#22551) (#22552) + * Mute all links in issue timeline (#22534) + +## [1.18.2](https://github.com/go-gitea/gitea/releases/tag/v1.18.2) - 2023-01-19 + +* BUGFIXES + * When updating by rebase we need to set the environment for head repo (#22535) (#22536) + * Fix issue not auto-closing when it includes a reference to a branch (#22514) (#22521) + * Fix invalid issue branch reference if not specified in template (#22513) (#22520) + * Fix 500 error viewing pull request when fork has pull requests disabled (#22512) (#22515) + * Reliable selection of admin user (#22509) (#22511) + * Set disable_gravatar/enable_federated_avatar when offline mode is true (#22479) (#22496) +* BUILD + * cgo cross-compile for freebsd (#22397) (#22519) + +## [1.18.1](https://github.com/go-gitea/gitea/releases/tag/v1.18.1) - 2023-01-17 + +* API + * Add `sync_on_commit` option for push mirrors api (#22271) (#22292) +* BUGFIXES + * Update `github.com/zeripath/zapx/v15` (#22485) + * Fix pull request API field `closed_at` always being `null` (#22482) (#22483) + * Fix container blob mount (#22226) (#22476) + * Fix error when calculating repository size (#22392) (#22474) + * Fix Operator does not exist bug on explore page with ONLY_SHOW_RELEVANT_REPOS (#22454) (#22472) + * Fix environments for KaTeX and error reporting (#22453) (#22473) + * Remove the netgo tag for Windows build (#22467) (#22468) + * Fix migration from GitBucket (#22477) (#22465) + * Prevent panic on looking at api "git" endpoints for empty repos (#22457) (#22458) + * Fix PR status layout on mobile (#21547) (#22441) + * Fix wechatwork webhook sends empty content in PR review (#21762) (#22440) + * Remove duplicate "Actions" label in mobile view (#21974) (#22439) + * Fix leaving organization bug on user settings -> orgs (#21983) (#22438) + * Fixed colour transparency regex matching in project board sorting (#22092) (#22437) + * Correctly handle select on multiple channels in Queues (#22146) (#22428) + * Prepend refs/heads/ to issue template refs (#20461) (#22427) + * Restore function to "Show more" buttons (#22399) (#22426) + * Continue GCing other repos on error in one repo (#22422) (#22425) + * Allow HOST has no port (#22280) (#22409) + * Fix omit avatar_url in discord payload when empty (#22393) (#22394) + * Don't display stop watch top bar icon when disabled and hidden when click other place (#22374) (#22387) + * Don't lookup mail server when using sendmail (#22300) (#22383) + * Fix gravatar disable bug (#22337) + * Fix update settings table on install (#22326) (#22327) + * Fix sitemap (#22272) (#22320) + * Fix code search title translation (#22285) (#22316) + * Fix due date rendering the wrong date in issue (#22302) (#22306) + * Fix get system setting bug when enabled redis cache (#22298) + * Fix bug of DisableGravatar default value (#22297) + * Fix key signature error page (#22229) (#22230) +* TESTING + * Remove test session cache to reduce possible concurrent problem (#22199) (#22429) +* MISC + * Restore previous official review when an official review is deleted (#22449) (#22460) + * Log STDERR of external renderer when it fails (#22442) (#22444) + +## [1.18.0](https://github.com/go-gitea/gitea/releases/tag/1.18.0) - 2022-12-22 + +* SECURITY + * Remove ReverseProxy authentication from the API (#22219) (#22251) + * Support Go Vulnerability Management (#21139) + * Forbid HTML string tooltips (#20935) +* BREAKING + * Rework mailer settings (#18982) + * Remove U2F support (#20141) + * Refactor `i18n` to `locale` (#20153) + * Enable contenthash in filename for dynamic assets (#20813) +* FEATURES + * Add color previews in markdown (#21474) + * Allow package version sorting (#21453) + * Add support for Chocolatey/NuGet v2 API (#21393) + * Add API endpoint to get changed files of a PR (#21177) + * Add filetree on left of diff view (#21012) + * Support Issue forms and PR forms (#20987) + * Add support for Vagrant packages (#20930) + * Add support for `npm unpublish` (#20688) + * Add badge capabilities to users (#20607) + * Add issue filter for Author (#20578) + * Add KaTeX rendering to Markdown. (#20571) + * Add support for Pub packages (#20560) + * Support localized README (#20508) + * Add support mCaptcha as captcha provider (#20458) + * Add team member invite by email (#20307) + * Added email notification option to receive all own messages (#20179) + * Switch Unicode Escaping to a VSCode-like system (#19990) + * Add user/organization code search (#19977) + * Only show relevant repositories on explore page (#19361) + * User keypairs and HTTP signatures for ActivityPub federation using go-ap (#19133) + * Add sitemap support (#18407) + * Allow creation of OAuth2 applications for orgs (#18084) + * Add system setting table with cache and also add cache supports for user setting (#18058) + * Add pages to view watched repos and subscribed issues/PRs (#17156) + * Support Proxy protocol (#12527) + * Implement sync push mirror on commit (#19411) +* API + * Allow empty assignees on pull request edit (#22150) (#22214) + * Make external issue tracker regexp configurable via API (#21338) + * Add name field for org api (#21270) + * Show teams with no members if user is admin (#21204) + * Add latest commit's SHA to content response (#20398) + * Add allow_rebase_update, default_delete_branch_after_merge to repository api response (#20079) + * Add new endpoints for push mirrors management (#19841) +* ENHANCEMENTS + * Add setting to disable the git apply step in test patch (#22130) (#22170) + * Multiple improvements for comment edit diff (#21990) (#22007) + * Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21928) + * Fix flex layout for repo list icons (#21896) (#21920) + * Fix vertical align of committer avatar rendered by email address (#21884) (#21918) + * Fix setting HTTP headers after write (#21833) (#21877) + * Color and Style enhancements (#21784, #21799) (#21868) + * Ignore line anchor links with leading zeroes (#21728) (#21776) + * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738) + * Use CSS color-scheme instead of invert (#21616) (#21623) + * Respect user's locale when rendering the date range in the repo activity page (#21410) + * Change `commits-table` column width (#21564) + * Refactor git command arguments and make all arguments to be safe to be used (#21535) + * CSS color enhancements (#21534) + * Add link to user profile in markdown mention only if user exists (#21533, #21554) + * Add option to skip index dirs (#21501) + * Diff file tree tweaks (#21446) + * Localize all timestamps (#21440) + * Add `code` highlighting in issue titles (#21432) + * Use Name instead of DisplayName in LFS Lock (#21415) + * Consolidate more CSS colors into variables (#21402) + * Redirect to new repository owner (#21398) + * Use ISO date format instead of hard-coded English date format for date range in repo activity page (#21396) + * Use weighted algorithm for string matching when finding files in repo (#21370) + * Show private data in feeds (#21369) + * Refactor parseTreeEntries, speed up tree list (#21368) + * Add GET and DELETE endpoints for Docker blob uploads (#21367) + * Add nicer error handling on template compile errors (#21350) + * Add `stat` to `ToCommit` function for speed (#21337) + * Support instance-wide OAuth2 applications (#21335) + * Record OAuth client type at registration (#21316) + * Add new CSS variables --color-accent and --color-small-accent (#21305) + * Improve error descriptions for unauthorized_client (#21292) + * Case-insensitive "find files in repo" (#21269) + * Consolidate more CSS rules, fix inline code on arc-green (#21260) + * Log real ip of requests from ssh (#21216) + * Save files in local storage as group readable (#21198) + * Enable fluid page layout on medium size viewports (#21178) + * File header tweaks (#21175) + * Added missing headers on user packages page (#21172) + * Display image digest for container packages (#21170) + * Skip dirty check for team forms (#21154) + * Keep path when creating a new branch (#21153) + * Remove fomantic image module (#21145) + * Make labels clickable in the comments section. (#21137) + * Sort branches and tags by date descending (#21136) + * Better repo API unit checks (#21130) + * Improve commit status icons (#21124) + * Limit length of repo description and repo url input fields (#21119) + * Show .editorconfig errors in frontend (#21088) + * Allow poster to choose reviewers (#21084) + * Remove black labels and CSS cleanup (#21003) + * Make e-mail sanity check more precise (#20991) + * Use native inputs in whitespace dropdown (#20980) + * Enhance package date display (#20928) + * Display total blob size of a package version (#20927) + * Show language name on hover (#20923) + * Show instructions for all generic package files (#20917) + * Refactor AssertExistsAndLoadBean to use generics (#20797) + * Move the official website link at the footer of gitea (#20777) + * Add support for full name in reverse proxy auth (#20776) + * Remove useless JS operation for relative time tooltips (#20756) + * Replace some icons with SVG (#20741) + * Change commit status icons to SVG (#20736) + * Improve single repo action for issue and pull requests (#20730) + * Allow multiple files in generic packages (#20661) + * Add option to create new issue from /issues page (#20650) + * Background color of private list-items updated (#20630) + * Added search input field to issue filter (#20623) + * Increase default item listing size `ISSUE_PAGING_NUM` to 20 (#20547) + * Modify milestone search keywords to be case insensitive again (#20513) + * Show hint to link package to repo when viewing empty repo package list (#20504) + * Add Tar ZSTD support (#20493) + * Make code review checkboxes clickable (#20481) + * Add "X-Gitea-Object-Type" header for GET `/raw/` & `/media/` API (#20438) + * Display project in issue list (#20434) + * Prepend commit message to template content when opening a new PR (#20429) + * Replace fomantic popup module with tippy.js (#20428) + * Allow to specify colors for text in markup (#20363) + * Allow access to the Public Organization Member lists with minimal permissions (#20330) + * Use default values when provided values are empty (#20318) + * Vertical align navbar avatar at middle (#20302) + * Delete cancel button in repo creation page (#21381) + * Include login_name in adminCreateUser response (#20283) + * fix: icon margin in user/settings/repos (#20281) + * Remove blue text on migrate page (#20273) + * Modify milestone search keywords to be case insensitive (#20266) + * Move some files into models' sub packages (#20262) + * Add tooltip to repo icons in explore page (#20241) + * Remove deprecated licenses (#20222) + * Webhook for Wiki changes (#20219) + * Share HTML template renderers and create a watcher framework (#20218) + * Allow enable LDAP source and disable user sync via CLI (#20206) + * Adds a checkbox to select all issues/PRs (#20177) + * Refactor `i18n` to `locale` (#20153) + * Disable status checks in template if none found (#20088) + * Allow manager logging to set SQL (#20064) + * Add order by for assignee no sort issue (#20053) + * Take a stab at porting existing components to Vue3 (#20044) + * Add doctor command to write commit-graphs (#20007) + * Add support for authentication based on reverse proxy email (#19949) + * Enable spellcheck for EasyMDE, use contenteditable mode (#19776) + * Allow specifying SECRET_KEY_URI, similar to INTERNAL_TOKEN_URI (#19663) + * Rework mailer settings (#18982) + * Add option to purge users (#18064) + * Add author search input (#21246) + * Make rss/atom identifier globally unique (#21550) +* BUGFIXES + * Auth interface return error when verify failure (#22119) (#22259) + * Use complete SHA to create and query commit status (#22244) (#22257) + * Update bleve and zapx to fix unaligned atomic (#22031) (#22218) + * Prevent panic in doctor command when running default checks (#21791) (#21807) + * Load GitRepo in API before deleting issue (#21720) (#21796) + * Ignore line anchor links with leading zeroes (#21728) (#21776) + * Set last login when activating account (#21731) (#21755) + * Fix UI language switching bug (#21597) (#21749) + * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738) + * Allow local package identifiers for PyPI packages (#21690) (#21727) + * Deal with markdown template without metadata (#21639) (#21654) + * Fix opaque background on mermaid diagrams (#21642) (#21652) + * Fix repository adoption on Windows (#21646) (#21650) + * Sync git hooks when config file path changed (#21619) (#21626) + * Fix 500 on PR files API (#21602) (#21607) + * Fix `Timestamp.IsZero` (#21593) (#21603) + * Fix viewing user subscriptions (#21482) + * Fix mermaid-related bugs (#21431) + * Fix branch dropdown shifting on page load (#21428) + * Fix default theme-auto selector when nologin (#21346) + * Fix and improve incorrect error messages (#21342) + * Fix formatted link for PR review notifications to matrix (#21319) + * Center-aligning content of WebAuthN page (#21127) + * Remove follow from commits by file (#20765) + * Fix commit status popup (#20737) + * Fix init mail render logic (#20704) + * Use correct page size for link header pagination (#20546) + * Preserve unix socket file (#20499) + * Use tippy.js for context popup (#20393) + * Add missing parameter for error in log message (#20144) + * Do not allow organisation owners add themselves as collaborator (#20043) + * Rework file highlight rendering and fix yaml copy-paste (#19967) + * Improve code diff highlight, fix incorrect rendered diff result (#19958) +* TESTING + * Improve OAuth integration tests (#21390) + * Add playwright tests (#20123) +* BUILD + * Switch to building with go1.19 (#20695) + * Update JS dependencies, adjust eslint (#20659) + * Add more linters to improve code readability (#19989) + +## [1.17.4](https://github.com/go-gitea/gitea/releases/tag/1.17.4) - 2022-12-21 + +* SECURITY + * Do not allow Ghost access to limited visible user/org (#21849) (#21875) + * Fix package access for admins and inactive users (#21580) (#21592) +* ENHANCEMENTS + * Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21927) + * Fix vertical align of committer avatar rendered by email address (#21884) (#21919) + * Fix setting HTTP headers after write (#21833) (#21874) + * Ignore line anchor links with leading zeroes (#21728) (#21777) + * Enable Monaco automaticLayout (#21516) +* BUGFIXES + * Do not list active repositories as unadopted (#22034) (#22167) + * Correctly handle moved files in apply patch (#22118) (#22136) + * Fix condition for is_internal (#22095) (#22131) + * Fix permission check on issue/pull lock (#22114) + * Fix sorting admin user list by last login (#22081) (#22106) + * Workaround for container registry push/pull errors (#21862) (#22069) + * Fix issue/PR numbers (#22037) (#22045) + * Handle empty author names (#21902) (#22028) + * Fix ListBranches to handle empty case (#21921) (#22025) + * Fix enabling partial clones on 1.17 (#21809) + * Prevent panic in doctor command when running default checks (#21791) (#21808) + * Upgrade golang.org/x/crypto (#21792) (#21794) + * Init git module before database migration (#21764) (#21766) + * Set last login when activating account (#21731) (#21754) + * Add HEAD fix to gitea doctor (#21352) (#21751) + * Fix UI language switching bug (#21597) (#21748) + * Remove semver compatible flag and change pypi to an array of test cases (#21708) (#21729) + * Allow local package identifiers for PyPI packages (#21690) (#21726) + * Fix repository adoption on Windows (#21646) (#21651) + * Sync git hooks when config file path changed (#21619) (#21625) + * Added check for disabled Packages (#21540) (#21614) + * Fix `Timestamp.IsZero` (#21593) (#21604) + * Fix issues count bug (#21600) + * Support binary deploy in npm packages (#21589) + * Update milestone counters when issue is deleted (#21459) (#21586) + * SessionUser protection against nil pointer dereference (#21581) + * Case-insensitive NuGet symbol file GUID (#21409) (#21575) + * Suppress `ExternalLoginUserNotExist` error (#21504) (#21572) + * Prevent Authorization header for presigned LFS urls (#21531) (#21569) + * Update binding to fix bugs (#21560) + * Fix generating compare link (#21519) (#21530) + * Ignore error when retrieving changed PR review files (#21487) (#21524) + * Fix incorrect notification commit url (#21479) (#21483) + * Display total commit count in hook message (#21400) (#21481) + * Enforce grouped NuGet search results (#21442) (#21480) + * Return 404 when user is not found on avatar (#21476) (#21477) + * Normalize NuGet package version on upload (#22186) (#22201) +* MISC + * Check for zero time instant in TimeStamp.IsZero() (#22171) (#22173) + * Fix warn in database structs sync (#22111) + * Allow for resolution of NPM registry paths that match upstream (#21568) (#21723) + ## [1.17.3](https://github.com/go-gitea/gitea/releases/tag/v1.17.3) - 2022-10-15 * SECURITY diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbe418c356..c0a95c2678 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,449 +1,23 @@ -# Contribution Guidelines +# Forgejo Contributor Guide -## Table of Contents +The Forgejo project is run by a community of people who are expected to follow this guide when cooperating on a simple bug fix as well as when changing the governance. For more information about the project, take a look at [the documentation explaining what Forgejo provides](README.md). -- [Contribution Guidelines](#contribution-guidelines) - - [Table of Contents](#table-of-contents) - - [Introduction](#introduction) - - [Bug reports](#bug-reports) - - [Discuss your design](#discuss-your-design) - - [Testing redux](#testing-redux) - - [Vendoring](#vendoring) - - [Translation](#translation) - - [Building Gitea](#building-gitea) - - [Code review](#code-review) - - [Styleguide](#styleguide) - - [Design guideline](#design-guideline) - - [API v1](#api-v1) - - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) - - [Release Cycle](#release-cycle) - - [Maintainers](#maintainers) - - [Owners](#owners) - - [Versions](#versions) - - [Releasing Gitea](#releasing-gitea) - - [Copyright](#copyright) +Sensitive security-related issues should be reported to [security@forgejo.org](mailto:security@forgejo.org) using [encryption](https://keyoxide.org/security@forgejo.org). -## Introduction +## For everyone involved -This document explains how to contribute changes to the Gitea project. -It assumes you have followed the -[installation instructions](https://docs.gitea.io/en-us/). -Sensitive security-related issues should be reported to -[security@gitea.io](mailto:security@gitea.io). +- [Code of Conduct](CONTRIBUTING/COC.md) +- [Bugs, features, security and others discussions](CONTRIBUTING/DISCUSSIONS.md) +- [Governance](CONTRIBUTING/GOVERNANCE.md) +- [Sustainability and funding](https://codeberg.org/forgejo/sustainability/src/branch/master/README.md) -For configuring IDE or code editor to develop Gitea see [IDE and code editor configuration](contrib/ide/) +## For contributors -## Bug reports +- [Developer Certificate of Origin (DCO)](CONTRIBUTING/DCO.md) +- [Development workflow](CONTRIBUTING/WORKFLOW.md) -Please search the issues on the issue tracker with a variety of keywords -to ensure your bug is not already reported. +## For maintainers -If unique, [open an issue](https://github.com/go-gitea/gitea/issues/new) -and answer the questions so we can understand and reproduce the -problematic behavior. +- [Release management](CONTRIBUTING/RELEASE.md) +- [Secrets](CONTRIBUTING/SECRETS.md) -To show us that the issue you are having is in Gitea itself, please -write clear, concise instructions so we can reproduce the behavior— -even if it seems obvious. The more detailed and specific you are, -the faster we can fix the issue. Check out [How to Report Bugs -Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html). - -Please be kind, remember that Gitea comes at no cost to you, and you're -getting free help. - -## Discuss your design - -The project welcomes submissions. If you want to change or add something, -please let everyone know what you're working on—[file an issue](https://github.com/go-gitea/gitea/issues/new)! -Significant changes must go through the change proposal process -before they can be accepted. To create a proposal, file an issue with -your proposed changes documented, and make sure to note in the title -of the issue that it is a proposal. - -This process gives everyone a chance to validate the design, helps -prevent duplication of effort, and ensures that the idea fits inside -the goals for the project and tools. It also checks that the design is -sound before code is written; the code review tool is not the place for -high-level discussions. - -## Testing redux - -Before submitting a pull request, run all the tests for the whole tree -to make sure your changes don't cause regression elsewhere. - -Here's how to run the test suite: - -- code lint - -| | | -| :-------------------- | :---------------------------------------------------------------- | -|``make lint`` | lint everything (not suggest if you only change one type code) | -|``make lint-frontend`` | lint frontend files | -|``make lint-backend`` | lint backend files | - -- run test code (Suggest run in Linux) - -| | | -| :------------------------------------- | :----------------------------------------------- | -|``make test[\#TestSpecificName]`` | run unit test | -|``make test-sqlite[\#TestSpecificName]``| run [integration](tests/integration) test for SQLite | -|[More details about integration tests](tests/integration/README.md) | -|``make test-e2e-sqlite[\#TestSpecificFileName]``| run [end-to-end](tests/e2e) test for SQLite | -|[More details about e2e tests](tests/e2e/README.md) | - -## Vendoring - -We manage dependencies via [Go Modules](https://golang.org/cmd/go/#hdr-Module_maintenance), more details: [go mod](https://go.dev/ref/mod). - -Pull requests should only include `go.mod`, `go.sum` updates if they are part of -the same change, be it a bugfix or a feature addition. - -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. - -You can find more information on how to get started with it on the [Modules Wiki](https://github.com/golang/go/wiki/Modules). - -## Translation - -We do all translation work inside [Crowdin](https://crowdin.com/project/gitea). -The only translation that is maintained in this Git repository is -[`en_US.ini`](https://github.com/go-gitea/gitea/blob/master/options/locale/locale_en-US.ini) -and is synced regularly to Crowdin. Once a translation has reached -A SATISFACTORY PERCENTAGE it will be synced back into this repo and -included in the next released version. - -## Building Gitea - -See the [hacking instructions](https://docs.gitea.io/en-us/hacking-on-gitea/). - -## Code review - -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. We use GitHub's -pull request workflow to do that. And, we also use [LGTM](http://lgtm.co) -to ensure every PR is reviewed by at least 2 maintainers. - -Please try to make your pull request easy to review for us. And, please read -the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)* guide; -it has lots of useful tips for any project you may want to contribute. -Some of the key points: - -- Make small pull requests. The smaller, the faster to review and the - more likely it will be merged soon. -- Don't make changes unrelated to your PR. Maybe there are typos on - some comments, maybe refactoring would be welcome on a function... but - if that is not related to your PR, please make *another* PR for that. -- Split big pull requests into multiple small ones. An incremental change - will be faster to review than a huge PR. -- Use the first comment as a summary explainer of your PR and you should keep this up-to-date as the PR evolves. - -If your PR could cause a breaking change you must add a BREAKING section to this comment e.g.: - -``` -## :warning: BREAKING :warning: -``` - -To explain how this could affect users and how to mitigate these changes. - -Once code review starts on your PR, do not rebase nor squash your branch as it makes it -difficult to review the new changes. Only if there is a need, sync your branch by merging -the base branch into yours. Don't worry about merge commits messing up your tree as -the final merge process squashes all commits into one, with the visible commit message (first -line) being the PR title + PR index and description being the PR's first comment. - -Once your PR gets the `lgtm/done` label, don't worry about keeping it up-to-date or breaking -builds (unless there's a merge conflict or a request is made by a maintainer to make -modifications). It is the maintainer team's responsibility from this point to get it merged. - -## Styleguide - -For imports you should use the following format (*without* the comments) - -```go -import ( - // stdlib - "fmt" - "math" - - // local packages - "code.gitea.io/gitea/models" - "code.gitea.io/sdk/gitea" - - // external packages - "github.com/foo/bar" - "gopkg.io/baz.v1" -) -``` - -## Design guideline - -To maintain understandable code and avoid circular dependencies it is important to have a good structure of the code. The Gitea code is divided into the following parts: - -- **models:** Contains the data structures used by xorm to construct database tables. It also contains supporting functions to query and update the database. Dependencies to other code in Gitea should be avoided although some modules might be needed (for example for logging). -- **models/fixtures:** Sample model data used in integration tests. -- **models/migrations:** Handling of database migrations between versions. PRs that changes a database structure shall also have a migration step. -- **modules:** Different modules to handle specific functionality in Gitea. Shall only depend on other modules but not other packages (models, services). -- **public:** Frontend files (javascript, images, css, etc.) -- **routers:** Handling of server requests. As it uses other Gitea packages to serve the request, other packages (models, modules or services) shall not depend on routers. -- **services:** Support functions for common routing operations. Uses models and modules to handle the request. -- **templates:** Golang templates for generating the html output. -- **tests/e2e:** End to end tests -- **tests/integration:** Integration tests -- **vendor:** External code that Gitea depends on. - -## Documentation - -If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated. - -## API v1 - -The API is documented by [swagger](http://try.gitea.io/api/swagger) and is based on [GitHub API v3](https://developer.github.com/v3/). - -Thus, Gitea´s API should use the same endpoints and fields as GitHub´s 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 APIv2 (which is currently not planned). - -All expected results (errors, success, fail messages) should 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 the following: -([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 the following: -([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279)) - -In general, HTTP methods are chosen as follows: - -- **GET** endpoints return requested object and status **OK (200)** -- **DELETE** endpoints return status **No Content (204)** -- **POST** endpoints return status **Created (201)**, used to **create** new objects (e.g. a User) -- **PUT** endpoints return status **No Content (204)**, used to **add/assign** existing Objects (e.g. User) to something (e.g. Org-Team) -- **PATCH** endpoints return changed object and status **OK (200)**, used to **edit/change** an existing object - -An endpoint which changes/edits an object expects all fields to be optional (except ones to identify the object, which are required). - -### Endpoints returning lists should - -- 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 - -Occasionally backports of PRs are required. - -The backported PR title should be: - -``` -Title of backported PR (#ORIGINAL_PR_NUMBER) -``` - -The first two lines of the summary of the backporting PR should be: - -``` -Backport #ORIGINAL_PR_NUMBER - -``` - -with the rest of the summary matching the original PR. Similarly for frontports - ---- - -The below is a script that may be helpful in creating backports. YMMV. - -```bash -#!/bin/sh -PR="$1" -SHA="$2" -VERSION="$3" - -if [ -z "$SHA" ]; then - SHA=$(gh api /repos/go-gitea/gitea/pulls/$PR -q '.merge_commit_sha') -fi - -if [ -z "$VERSION" ]; then - VERSION="v1.16" -fi - -echo git checkout origin/release/"$VERSION" -b backport-$PR-$VERSION -git checkout origin/release/"$VERSION" -b backport-$PR-$VERSION -git cherry-pick $SHA && git commit --amend && git push zeripath backport-$PR-$VERSION && xdg-open https://github.com/go-gitea/gitea/compare/release/"$VERSION"...zeripath:backport-$PR-$VERSION - -``` - -## 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). No further action is required. -Additionally you could add a line at the end of your commit message. - -``` -Signed-off-by: Joe Smith -``` - -If you set your `user.name` and `user.email` Git configs, you can add the -line to the end of your commit 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 -minor 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. 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. A release is -maintained by issuing patch releases to only correct critical problems -such as crashes or security issues. - -Major release cycles are seasonal. They always begin on the 25th and end on -the 24th (i.e., the 25th of December to March 24th). - -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 [team -maintainers](MAINTAINERS). Every PR **MUST** be reviewed by at least -two maintainers (or owners) before it can get merged. A maintainer -should be a contributor of Gitea (or Gogs) and contributed at least -4 accepted PRs. A contributor should apply as a maintainer in the -[Discord](https://discord.gg/NsatcWJ) #develop channel. The owners -or 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/ - -## Owners - -Since Gitea is a pure community organization without any company support, -to keep the development healthy we will elect three owners every year. All -contributors may vote to elect up to three candidates, one of which will -be the main owner, and the other two the assistant owners. When the new -owners have been elected, the old owners will give up ownership to the -newly elected owners. If an owner is unable to do so, the other owners -will assist in ceding ownership to the newly elected owners. -For security reasons, Owners or any account with write access (like a bot) -must use 2FA. -https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ - -After the election, the new owners should proactively agree -with our [CONTRIBUTING](CONTRIBUTING.md) requirements in the -[Discord](https://discord.gg/NsatcWJ) #general channel. Below are the -words to speak: - -``` -I'm honored to having been elected an owner of Gitea, I agree with -[CONTRIBUTING](CONTRIBUTING.md). I will spend part of my time on Gitea -and lead the development of Gitea. -``` - -To honor the past owners, here's the history of the owners and the time -they served: - -- 2022-01-01 ~ 2022-12-31 - https://github.com/go-gitea/gitea/issues/17872 - - [Lunny Xiao](https://gitea.com/lunny) - - [Matti Ranta](https://gitea.com/techknowlogick) - - [Andrew Thornton](https://gitea.com/zeripath) - -- 2021-01-01 ~ 2021-12-31 - https://github.com/go-gitea/gitea/issues/13801 - - [Lunny Xiao](https://gitea.com/lunny) - - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - - [Matti Ranta](https://gitea.com/techknowlogick) - -- 2020-01-01 ~ 2020-12-31 - https://github.com/go-gitea/gitea/issues/9230 - - [Lunny Xiao](https://gitea.com/lunny) - - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - - [Matti Ranta](https://gitea.com/techknowlogick) - -- 2019-01-01 ~ 2019-12-31 - https://github.com/go-gitea/gitea/issues/5572 - - [Lunny Xiao](https://github.com/lunny) - - [Lauris Bukšis-Haberkorns](https://github.com/lafriks) - - [Matti Ranta](https://github.com/techknowlogick) - -- 2018-01-01 ~ 2018-12-31 - https://github.com/go-gitea/gitea/issues/3255 - - [Lunny Xiao](https://github.com/lunny) - - [Lauris Bukšis-Haberkorns](https://github.com/lafriks) - - [Kim Carlbäcker](https://github.com/bkcsoft) - -- 2016-11-04 ~ 2017-12-31 - - [Lunny Xiao](https://github.com/lunny) - - [Thomas Boerger](https://github.com/tboerger) - - [Kim Carlbäcker](https://github.com/bkcsoft) - -## Versions - -Gitea has the `main` branch as a tip branch and has version branches -such as `release/v0.9`. `release/v0.9` is a release branch and we will -tag `v0.9.0` for binary download. If `v0.9.0` has bugs, we will accept -pull requests on the `release/v0.9` branch and publish a `v0.9.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 against in about serval 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.io and GitHub releases. Once ACKed: - - bump the version of https://dl.gitea.io/gitea/version.json - - merge the blog post PR - - announce the release in discord `#announcements` - -## Copyright - -Code that you contribute should use the standard copyright header: - -``` -// Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. -``` - -Files in the repository contain copyright from the year they are added -to the year they are last changed. If the copyright author is changed, -just paste the header below the old one. diff --git a/CONTRIBUTING/COC.md b/CONTRIBUTING/COC.md new file mode 100644 index 0000000000..e31374b0e7 --- /dev/null +++ b/CONTRIBUTING/COC.md @@ -0,0 +1,31 @@ +# Code of Conduct, Well Being and Moderation teams + +Forgejo strives to be an inclusive project where everyone can participate in a safe environment. The **Well Being** team is doing its best to defuse tensions before they escalate and is available to answer all requests sent its way. When diplomacy fails, the **Moderation** team will be forced to act to put a stop to actions that are contrary to the [Code of Conduct](https://codeberg.org/forgejo/code-of-conduct). + +## Well Being and Moderation teams + +Temporary Well Being and Moderation teams [were appointed 10 November 2022](https://codeberg.org/forgejo/meta/issues/13). + +The moderation team will rely on this [Code of Conduct](https://codeberg.org/forgejo/code-of-conduct) when diplomacy fails. + +### Well Being + +Their goal is to defuse tensions. + +It has no power whatsover. The members are approved by the organization and trusted to: + +- Read all communications to detect tensions between people before they escalate. +- Do their best to defuse tensions. + +* https://codeberg.org/Gusted +* https://codeberg.org/dachary + +### Moderation + +Their goal is to enforce the [Code of Conduct](https://codeberg.org/forgejo/code-of-conduct) when diplomacy fails. + +It has the power to exclude people from a space. + +Their decisions must be logical, fact based and transparent to the Forgejo community who trust them with this responsibility. + +* https://codeberg.org/circlebuilder diff --git a/CONTRIBUTING/DCO.md b/CONTRIBUTING/DCO.md new file mode 100644 index 0000000000..5971e5bbd6 --- /dev/null +++ b/CONTRIBUTING/DCO.md @@ -0,0 +1,29 @@ +# Developer Certificate of Origin (DCO) + +Contributions to Forgejo, in all the repositories in the [Forgejo organization](https://codeberg.org/forgejo) are accepted provided the author agrees to the following Developer Certificate of Origin (DCO). + +``` +By making a contribution to Forgejo, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the Free Software license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate Free Software + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same Free Software license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the Free Software license(s) involved. +``` \ No newline at end of file diff --git a/CONTRIBUTING/DISCUSSIONS.md b/CONTRIBUTING/DISCUSSIONS.md new file mode 100644 index 0000000000..e8ceef67db --- /dev/null +++ b/CONTRIBUTING/DISCUSSIONS.md @@ -0,0 +1,18 @@ +# Bugs, features and discussions + +The [Forgejo issue tracker](https://codeberg.org/forgejo/forgejo/issues) is where **bugs** should be reported and **features** requested. + +Dedicated repositories in the [Forgejo organization](https://codeberg.org/forgejo) cover areas such as: +- the [website](https://codeberg.org/forgejo/website) +- the [Code of Conduct](https://codeberg.org/forgejo/code-of-conduct) +- the [sustainability and funding](https://codeberg.org/forgejo/sustainability). + +Other discussions regarding all **non technical aspects** of Forgejo, such as the governance, happen in the [meta issue tracker](https://codeberg.org/forgejo/meta/issues) and in the [matrix chatroom](https://matrix.to/#/#forgejo-chat:matrix.org). + +# Security + +The [security team](https://codeberg.org/forgejo/meta/src/branch/readme/TEAMS.md#security) takes care of security vulnerabilities. It handles sensitive security-related issues reported to [security@forgejo.org](mailto:security@forgejo.org) using [encryption](https://keyoxide.org/security@forgejo.org). + +The security team also keeps the content of the [security.txt](https://codeberg.org/forgejo/website/src/branch/main/public/.well-known/security.txt) file up to date. + +The private GPG key for `security@forgejo.org` is shared among all members of the security team and not stored online. diff --git a/CONTRIBUTING/GOVERNANCE.md b/CONTRIBUTING/GOVERNANCE.md new file mode 100644 index 0000000000..4a331fc2a5 --- /dev/null +++ b/CONTRIBUTING/GOVERNANCE.md @@ -0,0 +1,19 @@ +# Governance + +## Codeberg e.V. custodian of the domains + +The Forgejo [domains](https://codeberg.org/forgejo/meta/issues/41) are owned by the democratic non-profit dedicated to Free Software [Codeberg e.V.](https://codeberg.org/Codeberg/org/src/branch/main/en/bylaws.md). Forgejo is therefore ultimately under the control of Codeberg e.V. and its governance. However, although Codeberg e.V. is committed to use and host Forgejo, it is expected that Forgejo defines its own governance, in a way that is compatible with the Codeberg e.V. governance. + +## Forgejo Governance + +See our [decision-making system](https://codeberg.org/forgejo/meta/src/branch/readme/DECISION-MAKING.md) (contains team agreements and guidelines). + +Forgejo was bootstraped in November 2022 and is in the process of [defining its governance](https://codeberg.org/forgejo/meta/issues/19). The [first meeting happened November 24th](https://codeberg.org/forgejo/meta/issues/19#issuecomment-694460) and everyone is welcome to participate. + +## Interim Forgejo Governance + +While the governance is being defined, there was a need to establish an interim Forgejo governance for safeguarding credentials, enforcing the Code of Conduct and ensuring security vulnerabilities are handled responsibly for the Forgejo releases. + +All people with a role in the interim Forgejo governance pledge to resign as soon as the Forgejo governance is in place. + +The people and teams that are part of the interim governance are [listed publicly](https://codeberg.org/forgejo/meta/src/branch/readme/TEAMS.md). diff --git a/CONTRIBUTING/RELEASE.md b/CONTRIBUTING/RELEASE.md new file mode 100644 index 0000000000..5f47a34589 --- /dev/null +++ b/CONTRIBUTING/RELEASE.md @@ -0,0 +1,153 @@ +# Release management + +## Release numbering + +The Forgejo release numbers are composed of the Gitea release number followed by a dash and a serial number. For instance: + +* Gitea **v1.18.0** will be Forgejo **v1.18.0-0**, **v1.18.0-1**, etc + +The Gitea release candidates are suffixed with **-rcN** which is handled as a special case for packaging: although **X.Y.Z** is lexicographically lower than **X.Y.Z-rc1** is is considered greater. The Forgejo serial number must therefore be inserted before the **-rcN** suffix to preserve the expected version ordering. + +* Gitea **v1.18.0-rc0** will be Forgejo **v1.18.0-0-rc0**, **v1.18.0-1-rc0** +* Gitea **v1.18.0-rc1** will be Forgejo **v1.18.0-2-rc1**, **v1.18.0-3-rc1**, **v1.18.0-4-rc1** +* Gitea **v1.18.0** will be Forgejo **v1.18.0-5**, **v1.18.0-6**, **v1.18.0-7** +* etc. + +Because Forgejo is a soft fork of Gitea, it must retain the same release numbering scheme to be compatible with libraries and tools that depend on it. For instance, the tea CLI or the Gitea SDK will behave differently depending on the server version they connect to. If Forgejo had a different numbering scheme, it would no longer be compatible with the Gitea ecosystem. + +From a [Semantic Versioning](https://semver.org/) standpoint, all Forgejo releases are [pre-releases](https://semver.org/#spec-item-9) because they are suffixed with a dash. They are syntactically correct but do not comply with the Semantic Versioning recommendations. Gitea is not compliant either and as long as Forgejo is a soft fork, it inherits this problem. + +## Release process + +When publishing the vX.Y.Z-N release, the following steps must be followed: + +### Create a milestone and a check list + +* Create a `Forgejo vX.X.Z-N` milestone set to the date of the release +* Create an issue named `[RELEASE] Forgejo vX.Y.Z-N` with a description that includes a list of what needs to be done for the release with links to follow the progress +* Set the milestone of this issue to `Forgejo vX.X.Z-N` +* Close the milestone when the release is complete + +### Cherry pick the latest commits from Gitea + +The vX.Y/forgejo branch is populated as part of the [rebase on top of Gitea](WORKFLOW.md). The release happens in between rebase and it is worth checking if the matching Gitea branch, release/vX.Y contains commits that should be included in the release. + +* `cherry-pick -x` the commits +* push the vX.Y/forgejo branch including the commits +* verify that the tests pass + +### Release Notes + +* Add an entry in RELEASE-NOTES.md +* Copy/paste the matching entry from CHANGELOG.md +* Update the PR references prefixing them with https://github.com/go-gitea/gitea/pull/ + +### Testing + +When Forgejo is released, artefacts (packages, binaries, etc.) are first published by the CI/CD pipelines in the https://codeberg.org/forgejo-experimental organization, to be downloaded and verified to work. + +* Push the vX.Y/forgejo branch to https://codeberg.org/forgejo-integration/forgejo +* Push the vX.Y.Z-N tag to https://codeberg.org/forgejo-integration (if it fails for whatever reason, the tag and the release can be removed manually) + * Binaries are built and uploaded to https://codeberg.org/forgejo/forgejo-integration/releases + * Container images are built and uploaded to https://codeberg.org/forgejo-integration/-/packages/container/forgejo/versions +* Push the vX.Y/forgejo branch to https://codeberg.org/forgejo-experimental/forgejo +* Push the vX.Y/forgejo branch to https://codeberg.org/forgejo/experimental +* Push the vX.Y.Z-N tag to https://codeberg.org/forgejo/experimental + * Binaries are downloaded from https://codeberg.org/forgejo-integration, signed and copied to https://codeberg.org/forgejo-experimental + * Container images are copied from https://codeberg.org/forgejo-integration to https://codeberg.org/forgejo-experimental +* Fetch the Forgejo release as part of the [forgejo-ci](https://codeberg.org/Codeberg-Infrastructure/scripted-configuration/src/branch/main/hosts/forgejo-ci) test suite. Push the change to a branch of a repository enabled in https://ci.dachary.org/ ([read more...](https://codeberg.org/forgejo/forgejo/issues/208)). It will deploy the release and run high level integration tests. +* Reach out to packagers and users to manually verify the release works as expected + +### Publication + +* Push the vX.Y.Z-N tag to https://codeberg.org/forgejo/release + * Binaries are downloaded from https://codeberg.org/forgejo-integration, signed and copied to https://codeberg.org/forgejo + * Container images are copied from https://codeberg.org/forgejo-integration to https://codeberg.org/forgejo + +### Website update + +* Restart the last CI build at https://codeberg.org/forgejo/website/src/branch/main/ +* Verify https://forgejo.org/download/ points to the expected release +* Manually try the instructions to work + +### DNS update + +* Update the `release.forgejo.org` TXT record that starts with `forgejo_versions=` to be `forgejo_versions=vX.Y.Z-N` + +### Standard toot + +The following toot can be re-used to announce a minor release at `https://floss.social/@forgejo`. For more significant releases it is best to consider a dedicated and non-standard toot. + +``` +#Forgejo vX.Y.Z-N was just released! This is a minor patch. Check out the release notes and download it at https://forgejo.org/releases/. If you experience any issues with this release, please report to https://codeberg.org/forgejo/forgejo/issues. +``` + +## Release signing keys management + +A GPG master key with no expiration date is created and shared with members of the Owners team via encrypted email. A subkey with a one year expiration date is created and stored in the secrets repository, to be used by the CI pipeline. The public master key is stored in the secrets repository and published where relevant. + +### Master key creation + +* gpg --expert --full-generate-key +* key type: ECC and ECC option with Curve 25519 as curve +* no expiration +* id: Forgejo Releases +* gpg --export-secret-keys --armor EB114F5E6C0DC2BCDD183550A4B61A2DC5923710 and send via encrypted email to Owners +* gpg --export --armor EB114F5E6C0DC2BCDD183550A4B61A2DC5923710 > release-team-gpg.pub +* commit to the secret repository + +### Subkey creation and renewal + +* gpg --expert --edit-key EB114F5E6C0DC2BCDD183550A4B61A2DC5923710 +* addkey +* key type: ECC (signature only) +* key validity: one year +* create [an issue](https://codeberg.org/forgejo/forgejo/issues) to schedule the renewal + +#### 2023 + +* gpg --export --armor F7CBF02094E7665E17ED6C44E381BF3E50D53707 > 2023-release-team-gpg.pub +* gpg --export-secret-keys --armor F7CBF02094E7665E17ED6C44E381BF3E50D53707 > 2023-release-team-gpg +* commit to the secrets repository +* renewal issue https://codeberg.org/forgejo/forgejo/issues/58 + +### CI configuration + +In the Woodpecker CI configuration the following secrets must be set: + +* `releaseteamgpg` is the secret GPG key used to sign the releases +* `releaseteamuser` is the user name to authenticate with the Forgejo API and publish the releases +* `releaseteamtoken` is the token to authenticate `releaseteamuser` with the Forgejo API and publish the releases +* `domain` is `codeberg.org` + +## Users, organizations and repositories + +### Shared user: release-team + +The [release-team](https://codeberg.org/release-team) user publishes and signs all releases. The associated email is mailto:release@forgejo.org. + +The public GPG key used to sign the releases is [EB114F5E6C0DC2BCDD183550A4B61A2DC5923710](https://codeberg.org/release-team.gpg) `Forgejo Releases ` + +### Shared user: forgejo-ci + +The [forgejo-ci](https://codeberg.org/forgejo-ci) user is dedicated to https://forgejo-ci.codeberg.org/ and provides it with OAuth2 credentials it uses to run. + +### Shared user: forgejo-experimental-ci + +The [forgejo-experimental-ci](https://codeberg.org/forgejo-experimental-ci) user is dedicated to provide the application tokens used by Woodpecker CI repositories to build releases and publish them to https://codeberg.org/forgejo-experimental. It does not (and must not) have permission to publish releases at https://codeberg.org/forgejo. + +### Integration and experimental organization + +The https://codeberg.org/forgejo-integration organization is dedicated to integration testing. Its purpose is to ensure all artefacts can effectively be published and retrieved by the CI/CD pipelines. + +The https://codeberg.org/forgejo-experimental organization is dedicated to publishing experimental Forgejo releases. They are copied from the https://codeberg.org/forgejo-integration organization. + +The `forgejo-experimental-ci` user as well as all Forgejo contributors working on the CI/CD pipeline should be owners of both organizations. + +The https://codeberg.org/forgejo-integration/forgejo repository is coupled with a Woodpecker CI repository configured with the credentials provided by the https://codeberg.org/forgejo-experimental-ci user. It runs the pipelines found in `releases/woodpecker-build/*.yml` which builds and publishes an unsigned release in https://codeberg.org/forgejo-integration. + +### Experimental and release repositories + +The https://codeberg.org/forgejo/experimental private repository is coupled with a Woodpecker CI repository configured with the credentials provided by the https://codeberg.org/forgejo-experimental-ci user. It runs the pipelines found in `releases/woodpecker-publish/*.yml` which signs and copies a release from https://codeberg.org/forgejo-integration into https://codeberg.org/forgejo-experimental. + +The https://codeberg.org/forgejo/release private repository is coupled with a Woodpecker CI repository configured with the credentials provided by the https://codeberg.org/release-team user. It runs the pipelines found in `releases/woodpecker-publish/*.yml` which signs and copies a release from https://codeberg.org/forgejo-integration into https://codeberg.org/forgejo. diff --git a/CONTRIBUTING/SECRETS.md b/CONTRIBUTING/SECRETS.md new file mode 100644 index 0000000000..6823d9f2f6 --- /dev/null +++ b/CONTRIBUTING/SECRETS.md @@ -0,0 +1,56 @@ +# Secrets + +All Forgejo credentials are shared among the [secret keepers](https://codeberg.org/forgejo/meta/src/branch/readme/TEAMS.md#secrets-keeper) teams in a private repository with encrypted content. + +## Get started + +1. Make sure you have a GPG Key, or [create one](https://github.com/NicoHood/gpgit#12-key-generation) +2. Send someone else your public key and ask this person to add yourself as a recipient +``` +# Commands for the other person +$ gpg --import public_key.asc +# The following command will open a prompt, with the available public keys. +# Choose the one you just added and all secrets will be re-encrypted with this new key. +$ gopass recipients add +``` +3. [Install gopass](https://www.gopass.pw/#install) +> :warning: When installing on Ubuntu or Debian you can either download the deb package, install manually or build from source or use our APT repository ([github comment](https://github.com/gopasspw/gopass/issues/1849#issuecomment-802789285) with more information). +4. Clone this repo using `gopass` (the name and email are for `git config`) +``` +$ gopass clone git@codeberg.org:forgejo/gopass.git +``` +5. Check the consistency of the gopass storage +``` +$ gopass fsck +``` + +## Get a secret + +Show the whole secret file: +``` +$ gopass show ovh.com/manager +``` + +Copy the password in the clipboard: +``` +$ gopass show -c ovh.com/manager +``` + +Copy the `user` part of the secret in the clipboard: +``` +$ gopass show -c ovh.com/manager user +``` + +## Insert or edit a secret +``` +$ gopass edit ovh.com/manager +``` +In the editor, insert the password on the first line. +You may then add lines with a `key: value` syntax (`user: username` for instance). + +## Debugging and manual git operations + +The following command will show the location and status of the git repo (all git commands are available). +``` +$ gopass git status +``` diff --git a/CONTRIBUTING/WORKFLOW.md b/CONTRIBUTING/WORKFLOW.md new file mode 100644 index 0000000000..9ff6101fe5 --- /dev/null +++ b/CONTRIBUTING/WORKFLOW.md @@ -0,0 +1,124 @@ +# Development workflow + +Forgejo is a soft fork, i.e. a set of commits applied to the Gitea development branch and the stable branches. On a regular basis those commits are rebased and modified if necessary to keep working. All Forgejo commits are merged into a branch from which binary releases and packages are created and distributed. The development workflow is a set of conventions Forgejo developers are expected to follow to work together. + +Discussions on how the workflow should evolve happen [in the isssue tracker](https://codeberg.org/forgejo/forgejo/issues?type=all&state=open&labels=&milestone=0&assignee=0&q=%5BWORKFLOW%5D). + +## Naming conventions + +### Development + +* Gitea: main +* Forgejo: forgejo +* Feature branches: forgejo-feature-name + +### Stable + +* Gitea: release/vX.Y +* Forgejo: vX.Y/forgejo +* Feature branches: vX.Y/forgejo-feature-name + +### Soft fork history + +Before rebasing on top of Gitea, all branches are copied to `soft-fork/YYYY-MM-DD/` for safekeeping. Older `soft-fork/*/` branches are converted into references under the same name. Similar to how pull requests store their head, they do not clutter the list of branches but can be retrieved if needed with `git fetch +refs/soft-fork/*:refs/soft-fork/*`. Tooling to automate this archival process [is available](https://codeberg.org/forgejo-contrib/soft-fork-tools/src/branch/master/README.md#archive-branches). + +### Tags + +Because the branches are rebased on top of Gitea, only the latest tag will be found in a given branch. For instance `v1.18.0-1` won't be found in the `v1.18/forgejo` branch after it is rebased. + +## Rebasing + +### *Feature branch* + +The *Gitea* branches are mirrored with the Gitea development and stable branches. + +On a regular basis, each *Feature branch* is rebased against the base *Gitea* branch. + +### forgejo branch + +The latest *Gitea* branch resets the *forgejo* branch and all *Feature branches* are merged into it. + +If tests pass after pushing *forgejo* to the https://codeberg.org/forgejo-integration/forgejo repository, it can be pushed to the https://codeberg.org/forgejo/forgejo repository. + +If tests do not pass, an issue is filed to the *Feature branch* that fails the test. Once the issue is resolved, another round of rebasing starts. + +### Cherry picking and rebasing + +Because Forgejo is a soft fork of Gitea, the commits in feature branches need to be cherry-picked on top of their base branch. They cannot be rebased using `git rebase`, because their base branch has been rebased. + +Here is how the commits in the `forgejo-f3` branch can be cherry-picked on top of the latest `forgejo-development` branch: + +``` +$ git fetch --all +$ git remote get-url forgejo +git@codeberg.org:forgejo/forgejo.git +$ git checkout -b forgejo/forgejo-f3 +$ git reset --hard forgejo/forgejo-development +$ git cherry-pick $(git rev-list --reverse forgejo/soft-fork/2022-12-10/forgejo-development..forgejo/soft-fork/2022-12-10/forgejo-f3) +$ git push --force forgejo-f3 forgejo/forgejo-f3 +``` + +## Feature branches + +All *Feature branches* are based on the {vX.Y/,}forgejo-development branch which provides development tools and documentation. + +The `forgejo-development` branch is based on the {vX.Y/,}forgejo-ci branch which provides the Woodpecker CI configuration. + +The purpose of each *Feature branch* is documented below: + +### General purpose + +* [forgejo-ci](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-ci) based on [main](https://codeberg.org/forgejo/forgejo/src/branch/main) + Woodpecker CI configuration, including the release process. + * Backports: [v1.18/forgejo-ci](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-ci) + +* [forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-development) based on [forgejo-ci](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-ci) + Forgejo development tools and documentation. + * Backports: [v1.18/forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-development) + +### Dependency + +* [forgejo-dependency](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-dependency) based on [forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-development) + Each commit is prefixed with the name of dependency in uppercase, for instance **[GOTH]** or **[GITEA]**. They are standalone and implement either a bug fix or a feature that is in the process of being contributed to the dependency. It is better to contribute directly to the dependency instead of adding a commit to this branch but it is sometimes not possible, for instance when someone does not have a GitHub account. The author of the commit is responsible for rebasing and resolve conflicts. The ultimate goal of this branch is to be empty and it is expected that a continuous effort is made to reduce its content so that the technical debt it represents does not burden Forgejo long term. + * Backports: [v1.18/forgejo-dependency](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-dependency) + +### [Privacy](https://codeberg.org/forgejo/forgejo/issues?labels=83271) + +* [forgejo-privacy](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-privacy) based on [forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-development) + Customize Forgejo to have more privacy. + * Backports: [v1.18/forgejo-privacy](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-privacy) + +### Branding +* [forgejo-branding](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-branding) based on [forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-development) + Replacing upstream branding with Forgejo branding + +### [Internationalization](https://codeberg.org/forgejo/forgejo/issues?labels=82637) +* [forgejo-i18n](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-i18n) based on [forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-development) + Internationalization support for Forgejo with a workflow based on Weblate. + +### [Accessibility](https://codeberg.org/forgejo/forgejo/issues?labels=81214) +* Backports only: [v1.18/forgejo-a11y](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-a11y) based on [v1.18/forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-development) + Backport future upstream a11y improvements to the current release of Forgejo + +### [Federation](https://codeberg.org/forgejo/forgejo/issues?labels=79349) + +* [forgejo-federation](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-federation) based on [forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-development) + Federation support for Forgejo + +* [forgejo-f3](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-f3) based on [forgejo-development](https://codeberg.org/forgejo/forgejo/src/branch/forgejo-development) + [F3](https://lab.forgefriends.org/friendlyforgeformat/gof3) support for Forgejo + +## Pull requests and feature branches + +Most people who are used to contributing will be familiar with the workflow of sending a pull request against the default branch. When that happens the reviewer should change the base branch to the appropriate *Feature branch* instead. If the pull request does not fit in any *Feature branch*, the reviewer needs to make decision to either: + +* Decline the pull request because it is best contributed to Gitea +* Create a new *Feature branch* + +Returning contributors can figure out which *Feature branch* to base their pull request on using the list of *Feature branches*. + +## Granularity + +*Feature branches* can contain a number of commits grouped together, for instance for branding the documentation, the landing page and the footer. It makes it convenient for people working on that topic to get the big picture without browsing multiple branches. Creating a new *Feature branch* for each individual commit, while possible, is likely to be difficult to work with. + +Observing the granularity of the existing *Feature branches* is the best way to figure out what works and what does not. It requires adjustments from time to time depending on the number of contributors and the complexity of the Forgejo codebase that sits on top of Gitea. diff --git a/Dockerfile b/Dockerfile index d5d98e69a8..905c327c3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ RUN go build contrib/environment-to-ini/environment-to-ini.go FROM alpine:3.16 -LABEL maintainer="maintainers@gitea.io" +LABEL maintainer="contact@forgejo.org" EXPOSE 22 3000 diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 8c2b8e98c9..d3df54f731 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -24,13 +24,14 @@ RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ RUN go build contrib/environment-to-ini/environment-to-ini.go FROM alpine:3.16 -LABEL maintainer="maintainers@gitea.io" +LABEL maintainer="contact@forgejo.org" EXPOSE 2222 3000 RUN apk --no-cache add \ bash \ ca-certificates \ + dumb-init \ gettext \ git \ curl \ @@ -68,6 +69,6 @@ ENV HOME "/var/lib/gitea/git" VOLUME ["/var/lib/gitea", "/etc/gitea"] WORKDIR /var/lib/gitea -ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"] CMD [] diff --git a/LICENSE b/LICENSE index a8d4b49dd0..eeefaa717a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ +Copyright (c) 2022 The Forgejo Authors Copyright (c) 2016 The Gitea Authors Copyright (c) 2015 The Gogs Authors diff --git a/Makefile b/Makefile index f1b6790dc5..03d63db44f 100644 --- a/Makefile +++ b/Makefile @@ -28,8 +28,8 @@ XGO_VERSION := go-1.19.x AIR_PACKAGE ?= github.com/cosmtrek/air@v1.40.4 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.5.0 ERRCHECK_PACKAGE ?= github.com/kisielk/errcheck@v1.6.1 -GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.3.1 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.0 +GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.4.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.0 GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10 MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.0 @@ -83,7 +83,7 @@ ifneq ($(DRONE_TAG),) GITEA_VERSION ?= $(VERSION) else ifneq ($(DRONE_BRANCH),) - VERSION ?= $(subst release/v,,$(DRONE_BRANCH)) + VERSION ?= $(shell echo $(DRONE_BRANCH) | sed -e 's|v\([0-9.][0-9.]*\)/.*|\1|') else VERSION ?= main endif @@ -96,7 +96,10 @@ else endif endif -LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" +# SemVer +FORGEJO_VERSION := v2.0.0 + +LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" -X "code.gitea.io/gitea/routers/api/forgejo/v1.ForgejoVersion=$(FORGEJO_VERSION)" LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 @@ -147,11 +150,14 @@ ifdef DEPS_PLAYWRIGHT PLAYWRIGHT_FLAGS += --with-deps endif +FORGEJO_API_SPEC := public/forgejo/api.v1.yml + SWAGGER_SPEC := templates/swagger/v1_json.tmpl SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g SWAGGER_EXCLUDE := code.gitea.io/sdk SWAGGER_NEWLINE_COMMAND := -e '$$a\' +SWAGGER_SPEC_BRANDING := s|Gitea API|Forgejo API|g TEST_MYSQL_HOST ?= mysql:3306 TEST_MYSQL_DBNAME ?= testgitea @@ -207,6 +213,8 @@ help: @echo " - generate-license update license files" @echo " - generate-gitignore update gitignore files" @echo " - generate-manpage generate manpage" + @echo " - generate-forgejo-api generate the forgejo API from spec" + @echo " - forgejo-api-validate check if the forgejo API matches the specs" @echo " - generate-swagger generate the swagger spec from code comments" @echo " - swagger-validate check if the swagger spec is valid" @echo " - golangci-lint run golangci-lint linter" @@ -285,8 +293,7 @@ misspell-check: .PHONY: vet vet: @echo "Running go vet..." - @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet - @$(GO) vet -vettool=gitea-vet $(GO_PACKAGES) + @$(GO) vet $(GO_PACKAGES) .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @@ -297,6 +304,27 @@ ifneq "$(TAGS)" "$(shell cat $(TAGS_EVIDENCE) 2>/dev/null)" TAGS_PREREQ := $(TAGS_EVIDENCE) endif +OAPI_CODEGEN_PACKAGE ?= github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.12.4 +KIN_OPENAPI_CODEGEN_PACKAGE ?= github.com/getkin/kin-openapi/cmd/validate@v0.114.0 +FORGEJO_API_SERVER = routers/api/forgejo/v1/generated.go + +.PHONY: generate-forgejo-api +generate-forgejo-api: $(FORGEJO_API_SPEC) + $(GO) run $(OAPI_CODEGEN_PACKAGE) -package v1 -generate chi-server,types $< > $(FORGEJO_API_SERVER) + +.PHONY: forgejo-api-check +forgejo-api-check: generate-forgejo-api + @diff=$$(git diff $(FORGEJO_API_SERVER) ; \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make generate-forgejo-api' and commit the result:"; \ + echo "$${diff}"; \ + exit 1; \ + fi + +.PHONY: forgejo-api-validate +forgejo-api-validate: + $(GO) run $(KIN_OPENAPI_CODEGEN_PACKAGE) $(FORGEJO_API_SPEC) + .PHONY: generate-swagger generate-swagger: $(SWAGGER_SPEC) @@ -304,6 +332,7 @@ $(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(GO) run $(SWAGGER_PACKAGE) generate spec -x "$(SWAGGER_EXCLUDE)" -o './$(SWAGGER_SPEC)' $(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)' $(SED_INPLACE) $(SWAGGER_NEWLINE_COMMAND) './$(SWAGGER_SPEC)' + $(SED_INPLACE) '$(SWAGGER_SPEC_BRANDING)' './$(SWAGGER_SPEC)' .PHONY: swagger-check swagger-check: generate-swagger @@ -332,7 +361,7 @@ checks: checks-frontend checks-backend checks-frontend: lockfile-check svg-check .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate +checks-backend: tidy-check swagger-check fmt-check misspell-check forgejo-api-validate swagger-validate .PHONY: lint lint: lint-frontend lint-backend @@ -358,7 +387,7 @@ watch-frontend: node-check node_modules .PHONY: watch-backend watch-backend: go-check - $(GO) run $(AIR_PACKAGE) -c .air.toml + GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml .PHONY: test test: test-frontend test-backend @@ -721,10 +750,14 @@ generate: generate-backend generate-backend: $(TAGS_PREREQ) generate-go .PHONY: generate-go -generate-go: $(TAGS_PREREQ) +generate-go: $(TAGS_PREREQ) merge-locales @echo "Running go generate..." @CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' $(GO_PACKAGES) +.PHONY: merge-locales +merge-locales: + $(GO) run build/merge-forgejo-locales.go + .PHONY: security-check security-check: govulncheck -v ./... @@ -733,16 +766,16 @@ $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@ .PHONY: release -release: frontend generate release-windows release-linux release-darwin release-copy release-compress vendor release-sources release-docs release-check +release: frontend generate release-linux release-copy release-compress vendor release-sources release-check $(DIST_DIRS): mkdir -p $(DIST_DIRS) .PHONY: release-windows release-windows: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . ifeq (,$(findstring gogit,$(TAGS))) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . endif ifeq ($(CI),true) cp /build/* $(DIST)/binaries @@ -750,7 +783,7 @@ endif .PHONY: release-linux release-linux: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out forgejo-$(VERSION) . ifeq ($(CI),true) cp /build/* $(DIST)/binaries endif @@ -762,6 +795,13 @@ ifeq ($(CI),true) cp /build/* $(DIST)/binaries endif +.PHONY: release-freebsd +release-freebsd: | $(DIST_DIRS) + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . +ifeq ($(CI),true) + cp /build/* $(DIST)/binaries +endif + .PHONY: release-copy release-copy: | $(DIST_DIRS) cd $(DIST); for file in `find . -type f -name "*"`; do cp $${file} ./release/; done; @@ -780,8 +820,8 @@ release-sources: | $(DIST_DIRS) # bsdtar needs a ^ to prevent matching subdirectories $(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./) # use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s - $(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./gitea-src-$(VERSION)/'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'")) - tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz . + $(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./forgejo-src-$(VERSION)/'" || echo "--transform 's|^./|forgejo-src-$(VERSION)/|'")) + tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/forgejo-src-$(VERSION).tar.gz . rm -f $(STORED_VERSION_FILE) .PHONY: release-docs @@ -874,13 +914,7 @@ lockfile-check: .PHONY: update-translations update-translations: - mkdir -p ./translations - cd ./translations && curl -L https://crowdin.com/download/project/gitea.zip > gitea.zip && unzip gitea.zip - rm ./translations/gitea.zip - $(SED_INPLACE) -e 's/="/=/g' -e 's/"$$//g' ./translations/*.ini - $(SED_INPLACE) -e 's/\\"/"/g' ./translations/*.ini - mv ./translations/*.ini ./options/locale/ - rmdir ./translations + # noop to detect merge conflicts (potentially needs updating the scripts) and avoid breaking with Gitea .PHONY: generate-license generate-license: diff --git a/README.md b/README.md index 19703d85dd..fa21427674 100644 --- a/README.md +++ b/README.md @@ -1,180 +1,47 @@ -

- - Gitea - -

-

Gitea - Git with a cup of tea

+
+ +

Welcome to Forgejo

+
-

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - Contribute with Gitpod - - - - - - - - - - -

+Hi there! Tired of big platforms playing monopoly? +Providing Git hosting for your project, friends, company or community? +**Forgejo** (/for'd͡ʒe.jo/ inspired by forĝejo – the Esperanto word for *forge*) has you covered with its intuitive interface, +light and easy hosting and a lot of builtin functionality. -

- View this document in Chinese -

+Forgejo was [created in 2022](https://forgejo.org/2022-12-15-hello-forgejo/) +because we think that the project should be owned by an independent community. +If you second that, then Forgejo is for you! +Our promise: **Independent Free/Libre Software forever!** -## Purpose +## What does Forgejo offer? -The goal of this project is to make the easiest, fastest, and most -painless way of setting up a self-hosted Git service. + -As Gitea is written in Go, it works across **all** the platforms and -architectures that are supported by Go, including Linux, macOS, and -Windows on x86, amd64, ARM and PowerPC architectures. -You can try it out using [the online demo](https://try.gitea.io/). -This project has been -[forked](https://blog.gitea.io/2016/12/welcome-to-gitea/) from -[Gogs](https://gogs.io) since November of 2016, but a lot has changed. +If you like any of the following, Forgejo is literally meant for you: -## Building +- Lightweight: Forgejo can easily be hosted on nearly **every machine**. + Running on a Raspberry? Small cloud instance? No problem! +- Project management: Besides Git hosting, Forgejo offers issues, + pull requests, wikis, kanban boards and much more to **coordinate with your team**. +- Publishing: Have something to share? Use **releases** to host your software for download, + or use the **package registry** to publish it for docker, npm and many other package managers. +- Customizable: Want to change your look? Change some settings? + There are many **config switches** to make Forgejo work exactly like you want. +- Powerful: Organizations & team permissions, CI integration, Code Search, LDAP, OAuth and much more. + If you have **advanced needs**, Forgejo has you covered. +- Privacy: From update checker to default settings: Forgejo is built to be **privacy first** for you and your crew. +- Federation: (WIP) We are actively working to connect software forges with each other through **ActivityPub**, + and create a collaborative network of personal instances. + Interested? [Read more](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CONTRIBUTING/WORKFLOW.md#federation-https-codeberg-org-forgejo-forgejo-issues-labels-79349) -From the root of the source tree, run: +## Learn more - TAGS="bindata" make build +Subscribe to releases and blog post on [our website](https://forgejo.org), find us on the Fediverse or hop into [our Matrix room](https://matrix.to/#/#forgejo-chat:matrix.org) if you have any questions or want to get involved. -or if SQLite support is required: - TAGS="bindata sqlite sqlite_unlock_notify" make build +## Get involved -The `build` target is split into two sub-targets: - -- `make backend` which requires [Go Stable](https://go.dev/dl/), required version is defined in [go.mod](/go.mod). -- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater and Internet connectivity to download npm dependencies. - -When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js and Internet connectivity. - -Parallelism (`make -j `) is not supported. - -More info: https://docs.gitea.io/en-us/install-from-source/ - -## Using - - ./gitea web - -NOTE: If you're interested in using our APIs, we have experimental -support with [documentation](https://try.gitea.io/api/swagger). - -## Contributing - -Expected workflow is: Fork -> Patch -> Push -> Pull Request - -NOTES: - -1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.** -2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks! - -## Translating - -Translations are done through Crowdin. If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there. - -You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up. - -https://docs.gitea.io/en-us/translation-guidelines/ - -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) - -## Further information - -For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.io/en-us/). -If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://discourse.gitea.io/). - -We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea). - -The Hugo-based documentation theme is hosted at [gitea/theme](https://gitea.com/gitea/theme). - -The official Gitea CLI is developed at [gitea/tea](https://gitea.com/gitea/tea). - -## Authors - -- [Maintainers](https://github.com/orgs/go-gitea/people) -- [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) -- [Translators](options/locale/TRANSLATORS) - -## Backers - -Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/gitea#backer)] - - - -## Sponsors - -Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/gitea#sponsor)] - - - - - - - - - - - - - -## FAQ - -**How do you pronounce Gitea?** - -Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g. - -**Why is this not hosted on a Gitea instance?** - -We're [working on it](https://github.com/go-gitea/gitea/issues/1029). - -## License - -This project is licensed under the MIT License. -See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file -for the full license text. - -## Screenshots - -Looking for an overview of the interface? Check it out! - -|![Dashboard](https://dl.gitea.io/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.io/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.io/screenshots/global_issues.png)| -|:---:|:---:|:---:| -|![Branches](https://dl.gitea.io/screenshots/branches.png)|![Web Editor](https://dl.gitea.io/screenshots/web_editor.png)|![Activity](https://dl.gitea.io/screenshots/activity.png)| -|![New Migration](https://dl.gitea.io/screenshots/migration.png)|![Migrating](https://dl.gitea.io/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) -![Pull Request Dark](https://dl.gitea.io/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.io/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.io/screenshots/diff_dark.png)| +If you are interested in making Forgejo better, either by reporting a bug or by changing the governance, please [take a look at the contribution guide](CONTRIBUTING.md). diff --git a/README_ZH.md b/README_ZH.md deleted file mode 100644 index 0e58ad6d4a..0000000000 --- a/README_ZH.md +++ /dev/null @@ -1,98 +0,0 @@ -

- - Gitea - -

-

Gitea - Git with a cup of tea

- -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - Contribute with Gitpod - - - - - - - - - - -

- -

- View this document in English -

- -## 目标 - -Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。 - -如果您想试用一下,请访问 [在线Demo](https://try.gitea.io/)! - -## 提示 - -1. **开始贡献代码之前请确保你已经看过了 [贡献者向导(英文)](CONTRIBUTING.md)**. -2. 所有的安全问题,请私下发送邮件给 **security@gitea.io**。谢谢! -3. 如果你要使用API,请参见 [API 文档](https://godoc.org/code.gitea.io/sdk/gitea). - -## 文档 - -关于如何安装请访问我们的 [文档站](https://docs.gitea.io/zh-cn/),如果没有找到对应的文档,你也可以通过 [Discord - 英文](https://discord.gg/gitea) 和 QQ群 328432459 来和我们交流。 - -## 贡献流程 - -Fork -> Patch -> Push -> Pull Request - -## 翻译 - -多语言翻译是基于Crowdin进行的. -[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) - -## 作者 - -* [Maintainers](https://github.com/orgs/go-gitea/people) -* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors) -* [Translators](options/locale/TRANSLATORS) - -## 授权许可 - -本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件中。 - -## 截图 - -|![Dashboard](https://dl.gitea.io/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.io/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.io/screenshots/global_issues.png)| -|:---:|:---:|:---:| -|![Branches](https://dl.gitea.io/screenshots/branches.png)|![Web Editor](https://dl.gitea.io/screenshots/web_editor.png)|![Activity](https://dl.gitea.io/screenshots/activity.png)| -|![New Migration](https://dl.gitea.io/screenshots/migration.png)|![Migrating](https://dl.gitea.io/screenshots/migration.gif)|![Pull Request View](https://image.ibb.co/e02dSb/6.png) -![Pull Request Dark](https://dl.gitea.io/screenshots/pull_requests_dark.png)|![Diff Review Dark](https://dl.gitea.io/screenshots/review_dark.png)|![Diff Dark](https://dl.gitea.io/screenshots/diff_dark.png)| diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 0000000000..1d4c525018 --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,427 @@ +# Release Notes + +A Forgejo release is published shortly after a Gitea release is published and they have [matching release numbers](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CONTRIBUTING/RELEASE.md#release-numbering). Additional Forgejo releases may be published to address urgent security issues or bug fixes. Forgejo release notes include all Gitea release notes. + +The Forgejo admin should carefully read the required manual actions before upgrading. A point release (e.g. v1.18.1 or v1.18.2) does not require manual actions but others might (e.g. v1.18.0, v1.19.0). + +## 1.18.3-2 + +This stable release includes a security fix for `git` and bug fixes. + +### Git + +Git [recently announced](https://github.blog/2023-02-14-git-security-vulnerabilities-announced-3/) new versions to address two CVEs ([CVE-2023-22490](https://cve.circl.lu/cve/CVE-2023-22490), [CVE-2023-23946](https://cve.circl.lu/cve/CVE-2023-23946)). On 14 Februrary 2023, Git published the maintenance release v2.39.2, together with releases for older maintenance tracks v2.38.4, v2.37.6, v2.36.5, v2.35.7, v2.34.7, v2.33.7, v2.32.6, v2.31.7, and v2.30.8. All major GNU/Linux distributions also provide updated packages via their security update channels. + +We recommend that all installations running a version affected by the issues described below are upgraded to the latest version as soon as possible. + +* When using a Forgejo binary: upgrade the `git` package to a version greater or equal to v2.39.2, v2.38.4, v2.37.6, v2.36.5, v2.35.7, v2.34.7, v2.33.7, v2.32.6, v2.31.7 or v2.30.8 +* When using a Forgejo container image: `docker pull codeberg.org/forgejo/forgejo:1.18.3-2` + +### Forgejo + +* BUGFIXES + * Use proxy for pull mirror (https://github.com/go-gitea/gitea/pull/22771) (https://github.com/go-gitea/gitea/pull/22772) + * Revert "Fixes accessibility of empty repository commit status" (https://github.com/go-gitea/gitea/pull/22632) + * A regression introduced in 1.18.3-1 prevented the CI status from displaying for commits with more than one pipeline +* FORGEJO RELEASE PROCESS BUGFIXES + * The tag SHA in the uploaded repository must match (https://codeberg.org/forgejo/forgejo/pulls/345) [Read more about the consequences of this on the Forgejo blog](https://forgejo.org/2023-02-12-tags/) + +### Gitea + +* BUGFIXES + * Load issue before accessing index in merge message (https://github.com/go-gitea/gitea/pull/22822) (https://github.com/go-gitea/gitea/pull/22830) + * Fix isAllowed of escapeStreamer (https://github.com/go-gitea/gitea/pull/22814) (https://github.com/go-gitea/gitea/pull/22837) + * Escape filename when assemble URL (https://github.com/go-gitea/gitea/pull/22850) (https://github.com/go-gitea/gitea/pull/22871) + * Fix PR file tree folders no longer collapsing (https://github.com/go-gitea/gitea/pull/22864) (https://github.com/go-gitea/gitea/pull/22872) + * Fix incorrect role labels for migrated issues and comments (https://github.com/go-gitea/gitea/pull/22914) (https://github.com/go-gitea/gitea/pull/22923) + * Fix blame view missing lines (https://github.com/go-gitea/gitea/pull/22826) (https://github.com/go-gitea/gitea/pull/22929) + * Fix 404 error viewing the LFS file (https://github.com/go-gitea/gitea/pull/22945) (https://github.com/go-gitea/gitea/pull/22948) +* FEATURES + * Add command to bulk set must-change-password (https://github.com/go-gitea/gitea/pull/22823) (https://github.com/go-gitea/gitea/pull/22928) + +## 1.18.3-1 + +This stable release includes bug fixes. + +### Forgejo + +* ACCESSIBILITY + * Add ARIA support for Fomantic UI checkboxes (https://github.com/go-gitea/gitea/pull/22599) + * Fixes accessibility behavior of Watching, Staring and Fork buttons (https://github.com/go-gitea/gitea/pull/22634) + * Add main landmark to templates and adjust titles (https://github.com/go-gitea/gitea/pull/22670) + * Improve checkbox accessibility a bit by adding the title attribute (https://github.com/go-gitea/gitea/pull/22593) + * Improve accessibility of navigation bar and footer (https://github.com/go-gitea/gitea/pull/22635) +* PRIVACY + * Use DNS queries to figure out the latest Forgejo version (https://codeberg.org/forgejo/forgejo/pulls/278) +* BRANDING + * Change the values for the nodeinfo API to correctly identify the software as Forgejo (https://codeberg.org/forgejo/forgejo/pulls/313) +* CI + * Use tagged test environment for stable branches (https://codeberg.org/forgejo/forgejo/pulls/318) + +### Gitea + +* BUGFIXES + * Fix missing message in git hook when pull requests disabled on fork (https://github.com/go-gitea/gitea/pull/22625) (https://github.com/go-gitea/gitea/pull/22658) + * add default user visibility to cli command "admin user create" (https://github.com/go-gitea/gitea/pull/22750) (https://github.com/go-gitea/gitea/pull/22760) + * Fix color of tertiary button on dark theme (https://github.com/go-gitea/gitea/pull/22739) (https://github.com/go-gitea/gitea/pull/22744) + * Fix restore repo bug, clarify the problem of ForeignIndex (https://github.com/go-gitea/gitea/pull/22776) (https://github.com/go-gitea/gitea/pull/22794) + * Escape path for the file list (https://github.com/go-gitea/gitea/pull/22741) (https://github.com/go-gitea/gitea/pull/22757) + * Fix bugs with WebAuthn preventing sign in and registration. (https://github.com/go-gitea/gitea/pull/22651) (https://github.com/go-gitea/gitea/pull/22721) +* PERFORMANCES + * Improve checkIfPRContentChanged (https://github.com/go-gitea/gitea/pull/22611) (https://github.com/go-gitea/gitea/pull/22644) + +## 1.18.3-0 + +This stable release includes bug fixes. + +### Forgejo + +* BUGFIXES + * Fix line spacing for plaintext previews (https://github.com/go-gitea/gitea/pull/22699) (https://github.com/go-gitea/gitea/pull/22701) + * Fix README TOC links (https://github.com/go-gitea/gitea/pull/22577) (https://github.com/go-gitea/gitea/pull/22677) + * Don't return duplicated users who can create org repo (https://github.com/go-gitea/gitea/pull/22560) (https://github.com/go-gitea/gitea/pull/22562) + * Link issue and pull requests status change in UI notifications directly to their event in the timelined view. (https://github.com/go-gitea/gitea/pull/22627) (https://github.com/go-gitea/gitea/pull/22642) + +### Gitea + +* BUGFIXES + * Add missing close bracket in imagediff (https://github.com/go-gitea/gitea/pull/22710) (https://github.com/go-gitea/gitea/pull/22712) + * Fix wrong hint when deleting a branch successfully from pull request UI (https://github.com/go-gitea/gitea/pull/22673) (https://github.com/go-gitea/gitea/pull/22698) + * Fix missing message in git hook when pull requests disabled on fork (https://github.com/go-gitea/gitea/pull/22625) (https://github.com/go-gitea/gitea/pull/22658) + +## 1.18.2-1 + +This stable release includes a security fix. It was possible to reveal a user's email address, which is problematic because users can choose to hide their email address from everyone. This was possible because the notification email for a repository transfer request to an organization included every user's email address in the owner team. This has been fixed by sending individual emails instead and the code was refactored to prevent it from happening again. + +We **strongly recommend** that all installations are upgraded to the latest version as soon as possible. + +### Gitea + +* BUGFIXES + * When updating by rebase we need to set the environment for head repo (https://github.com/go-gitea/gitea/pull/22535) (https://github.com/go-gitea/gitea/pull/22536) + * Mute all links in issue timeline (https://github.com/go-gitea/gitea/pull/22534) + * Truncate commit summary on repo files table. (https://github.com/go-gitea/gitea/pull/22551) (https://github.com/go-gitea/gitea/pull/22552) + * Prevent multiple `To` recipients (https://github.com/go-gitea/gitea/pull/22566) (https://github.com/go-gitea/gitea/pull/22569) + +## 1.18.2-0 + +This stable release includes bug fixes. + +### Gitea + +* BUGFIXES + * Fix issue not auto-closing when it includes a reference to a branch (https://github.com/go-gitea/gitea/pull/22514) (https://github.com/go-gitea/gitea/pull/22521) + * Fix invalid issue branch reference if not specified in template (https://github.com/go-gitea/gitea/pull/22513) (https://github.com/go-gitea/gitea/pull/22520) + * Fix 500 error viewing pull request when fork has pull requests disabled (https://github.com/go-gitea/gitea/pull/22512) (https://github.com/go-gitea/gitea/pull/22515) + * Reliable selection of admin user (https://github.com/go-gitea/gitea/pull/22509) (https://github.com/go-gitea/gitea/pull/22511) + +## 1.18.1-0 + +This is the first Forgejo stable point release. + +### Forgejo + +### Critical security update for Git + +Git [recently announced](https://github.blog/2023-01-17-git-security-vulnerabilities-announced-2/) new versions to address two CVEs ([CVE-2022-23521](https://cve.circl.lu/cve/CVE-2022-23521), [CVE-2022-41903](https://cve.circl.lu/cve/CVE-2022-41903)). On 17 January 2023, Git published the maintenance release v2.39.1, together with releases for older maintenance tracks v2.38.3, v2.37.5, v2.36.4, v2.35.6, v2.34.6, v2.33.6, v2.32.5, v2.31.6, and v2.30.7. All major GNU/Linux distributions also provide updated packages via their security update channels. + +We **strongly recommend** that all installations running a version affected by the issues described below are upgraded to the latest version as soon as possible. + +* When using a Forgejo binary: upgrade the `git` package to a version greater or equal to v2.39.1, v2.38.3, v2.37.5, v2.36.4, v2.35.6, v2.34.6, v2.33.6, v2.32.5, v2.31.6, or v2.30.7 +* When using a Forgejo container image: `docker pull codeberg.org/forgejo/forgejo:1.18.1-0` + +Read more in the [Forgejo blog](https://forgejo.org/2023-01-18-release-v1-18-1-0/). + +#### Release process stability + +The [release process](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-ci) based on [Woodpecker CI](https://woodpecker-ci.org/) was entirely reworked to be more resilient to transient errors. A new release is first uploaded into the new [Forgejo experimental](https://codeberg.org/forgejo-experimental/) organization for testing purposes. + +Automated end to end testing of releases was implemented with a full development cycle including the creation of a new repository and a run of CI. It relieves the user and developer from the burden of tedious manual testing. + +#### Container environment variables + +When running a container, all environment variables starting with `FORGEJO__` can be used instead of `GITEA__`. For backward compatibility with existing scripts, it is still possible to use `GITEA__` instead of `FORGEJO__`. For instance: + +``` +docker run --name forgejo -e FORGEJO__security__INSTALL_LOCK=true codeberg.org/forgejo/forgejo:1.18.1-0 +``` + +#### Forgejo hook types + +A new `forgejo` hook type is available and behaves exactly the same as the existing `gitea` hook type. It will be used to implement additional features specific to Forgejo in a way that will be backward compatible with Gitea. + +#### X-Forgejo headers + +Wherever a `X-Gitea` header is received or sent, an identical `X-Forgejo` is added. For instance when a notification mail is sent, the `X-Forgejo-Reason` header is set to explain why. Or when a webhook is sent, the `X-Forgejo-Event` header is set with `push`, `tag`, etc. for Woodpecker CI to decide on an action. + +#### Look and feel fixes + +The Forgejo theme was [modified](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-branding) to take into account user feedback. + +### Gitea + +* API + * Add `sync_on_commit` option for push mirrors api (https://github.com/go-gitea/gitea/pull/22271) (https://github.com/go-gitea/gitea/pull/22292) +* BUGFIXES + * Update `github.com/zeripath/zapx/v15` (https://github.com/go-gitea/gitea/pull/22485) + * Fix pull request API field `closed_at` always being `null` (https://github.com/go-gitea/gitea/pull/22482) (https://github.com/go-gitea/gitea/pull/22483) + * Fix container blob mount (https://github.com/go-gitea/gitea/pull/22226) (https://github.com/go-gitea/gitea/pull/22476) + * Fix error when calculating repository size (https://github.com/go-gitea/gitea/pull/22392) (https://github.com/go-gitea/gitea/pull/22474) + * Fix Operator does not exist bug on explore page with ONLY_SHOW_RELEVANT_REPOS (https://github.com/go-gitea/gitea/pull/22454) (https://github.com/go-gitea/gitea/pull/22472) + * Fix environments for KaTeX and error reporting (https://github.com/go-gitea/gitea/pull/22453) (https://github.com/go-gitea/gitea/pull/22473) + * Remove the netgo tag for Windows build (https://github.com/go-gitea/gitea/pull/22467) (https://github.com/go-gitea/gitea/pull/22468) + * Fix migration from GitBucket (https://github.com/go-gitea/gitea/pull/22477) (https://github.com/go-gitea/gitea/pull/22465) + * Prevent panic on looking at api "git" endpoints for empty repos (https://github.com/go-gitea/gitea/pull/22457) (https://github.com/go-gitea/gitea/pull/22458) + * Fix PR status layout on mobile (https://github.com/go-gitea/gitea/pull/21547) (https://github.com/go-gitea/gitea/pull/22441) + * Fix wechatwork webhook sends empty content in PR review (https://github.com/go-gitea/gitea/pull/21762) (https://github.com/go-gitea/gitea/pull/22440) + * Remove duplicate "Actions" label in mobile view (https://github.com/go-gitea/gitea/pull/21974) (https://github.com/go-gitea/gitea/pull/22439) + * Fix leaving organization bug on user settings -> orgs (https://github.com/go-gitea/gitea/pull/21983) (https://github.com/go-gitea/gitea/pull/22438) + * Fixed colour transparency regex matching in project board sorting (https://github.com/go-gitea/gitea/pull/22092) (https://github.com/go-gitea/gitea/pull/22437) + * Correctly handle select on multiple channels in Queues (https://github.com/go-gitea/gitea/pull/22146) (https://github.com/go-gitea/gitea/pull/22428) + * Prepend refs/heads/ to issue template refs (https://github.com/go-gitea/gitea/pull/20461) (https://github.com/go-gitea/gitea/pull/22427) + * Restore function to "Show more" buttons (https://github.com/go-gitea/gitea/pull/22399) (https://github.com/go-gitea/gitea/pull/22426) + * Continue GCing other repos on error in one repo (https://github.com/go-gitea/gitea/pull/22422) (https://github.com/go-gitea/gitea/pull/22425) + * Allow HOST has no port (https://github.com/go-gitea/gitea/pull/22280) (https://github.com/go-gitea/gitea/pull/22409) + * Fix omit avatar_url in discord payload when empty (https://github.com/go-gitea/gitea/pull/22393) (https://github.com/go-gitea/gitea/pull/22394) + * Don't display stop watch top bar icon when disabled and hidden when click other place (https://github.com/go-gitea/gitea/pull/22374) (https://github.com/go-gitea/gitea/pull/22387) + * Don't lookup mail server when using sendmail (https://github.com/go-gitea/gitea/pull/22300) (https://github.com/go-gitea/gitea/pull/22383) + * Fix gravatar disable bug (https://github.com/go-gitea/gitea/pull/22337) + * Fix update settings table on install (https://github.com/go-gitea/gitea/pull/22326) (https://github.com/go-gitea/gitea/pull/22327) + * Fix sitemap (https://github.com/go-gitea/gitea/pull/22272) (https://github.com/go-gitea/gitea/pull/22320) + * Fix code search title translation (https://github.com/go-gitea/gitea/pull/22285) (https://github.com/go-gitea/gitea/pull/22316) + * Fix due date rendering the wrong date in issue (https://github.com/go-gitea/gitea/pull/22302) (https://github.com/go-gitea/gitea/pull/22306) + * Fix get system setting bug when enabled redis cache (https://github.com/go-gitea/gitea/pull/22298) + * Fix bug of DisableGravatar default value (https://github.com/go-gitea/gitea/pull/22297) + * Fix key signature error page (https://github.com/go-gitea/gitea/pull/22229) (https://github.com/go-gitea/gitea/pull/22230) +* TESTING + * Remove test session cache to reduce possible concurrent problem (https://github.com/go-gitea/gitea/pull/22199) (https://github.com/go-gitea/gitea/pull/22429) +* MISC + * Restore previous official review when an official review is deleted (https://github.com/go-gitea/gitea/pull/22449) (https://github.com/go-gitea/gitea/pull/22460) + * Log STDERR of external renderer when it fails (https://github.com/go-gitea/gitea/pull/22442) (https://github.com/go-gitea/gitea/pull/22444) + +## 1.18.0-1 + +This is the first Forgejo release. + +### Forgejo improvements + +#### Woodpecker CI + +A new [CI configuration](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-ci) based on [Woodpecker CI](https://woodpecker-ci.org/) was created. It is used to: + +* run tests on every Forgejo pull request ([compliance](https://codeberg.org/forgejo/forgejo/src/tag/v1.18.0-1/.woodpecker/compliance.yml), [unit tests and integration tests](https://codeberg.org/forgejo/forgejo/src/tag/v1.18.0-1/.woodpecker/testing-amd64.yml)) +* publish the Forgejo v1.18.0-1 release, [as binary packages](https://codeberg.org/forgejo/forgejo/releases/tag/v1.18.0-1) for amd64, arm64 and armv6 and [container images](https://codeberg.org/forgejo/-/packages/container/forgejo/1.18.0-1) for amd64 and arm64, root and rootless + +#### Look and feel + +The default themes were replaced by Forgejo themes and the landing page was [modified](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-branding) to display the Forgejo logo and names but the look and feel remains otherwise identical to Gitea. + +Landing page + +#### Privacy + +Gitea instances fetch https://dl.gitea.io/gitea/version.json weekly by default, which raises privacy concerns. In Forgejo [this feature needs to be explicitly activated](https://codeberg.org/forgejo/forgejo/src/branch/v1.18/forgejo-privacy) at installation time or by modifying the configuration file. Forgejo also provides an alternative [RSS feed](https://forgejo.org/releases/) to be informed when a new release is published. + +### Gitea + +* SECURITY + * Remove ReverseProxy authentication from the API (https://github.com/go-gitea/gitea/pull/22219) (https://github.com/go-gitea/gitea/pull/22251) + * Support Go Vulnerability Management (https://github.com/go-gitea/gitea/pull/21139) + * Forbid HTML string tooltips (https://github.com/go-gitea/gitea/pull/20935) +* BREAKING + * Rework mailer settings (https://github.com/go-gitea/gitea/pull/18982) + * Remove U2F support (https://github.com/go-gitea/gitea/pull/20141) + * Refactor `i18n` to `locale` (https://github.com/go-gitea/gitea/pull/20153) + * Enable contenthash in filename for dynamic assets (https://github.com/go-gitea/gitea/pull/20813) +* FEATURES + * Add color previews in markdown (https://github.com/go-gitea/gitea/pull/21474) + * Allow package version sorting (https://github.com/go-gitea/gitea/pull/21453) + * Add support for Chocolatey/NuGet v2 API (https://github.com/go-gitea/gitea/pull/21393) + * Add API endpoint to get changed files of a PR (https://github.com/go-gitea/gitea/pull/21177) + * Add filetree on left of diff view (https://github.com/go-gitea/gitea/pull/21012) + * Support Issue forms and PR forms (https://github.com/go-gitea/gitea/pull/20987) + * Add support for Vagrant packages (https://github.com/go-gitea/gitea/pull/20930) + * Add support for `npm unpublish` (https://github.com/go-gitea/gitea/pull/20688) + * Add badge capabilities to users (https://github.com/go-gitea/gitea/pull/20607) + * Add issue filter for Author (https://github.com/go-gitea/gitea/pull/20578) + * Add KaTeX rendering to Markdown. (https://github.com/go-gitea/gitea/pull/20571) + * Add support for Pub packages (https://github.com/go-gitea/gitea/pull/20560) + * Support localized README (https://github.com/go-gitea/gitea/pull/20508) + * Add support mCaptcha as captcha provider (https://github.com/go-gitea/gitea/pull/20458) + * Add team member invite by email (https://github.com/go-gitea/gitea/pull/20307) + * Added email notification option to receive all own messages (https://github.com/go-gitea/gitea/pull/20179) + * Switch Unicode Escaping to a VSCode-like system (https://github.com/go-gitea/gitea/pull/19990) + * Add user/organization code search (https://github.com/go-gitea/gitea/pull/19977) + * Only show relevant repositories on explore page (https://github.com/go-gitea/gitea/pull/19361) + * User keypairs and HTTP signatures for ActivityPub federation using go-ap (https://github.com/go-gitea/gitea/pull/19133) + * Add sitemap support (https://github.com/go-gitea/gitea/pull/18407) + * Allow creation of OAuth2 applications for orgs (https://github.com/go-gitea/gitea/pull/18084) + * Add system setting table with cache and also add cache supports for user setting (https://github.com/go-gitea/gitea/pull/18058) + * Add pages to view watched repos and subscribed issues/PRs (https://github.com/go-gitea/gitea/pull/17156) + * Support Proxy protocol (https://github.com/go-gitea/gitea/pull/12527) + * Implement sync push mirror on commit (https://github.com/go-gitea/gitea/pull/19411) +* API + * Allow empty assignees on pull request edit (https://github.com/go-gitea/gitea/pull/22150) (https://github.com/go-gitea/gitea/pull/22214) + * Make external issue tracker regexp configurable via API (https://github.com/go-gitea/gitea/pull/21338) + * Add name field for org api (https://github.com/go-gitea/gitea/pull/21270) + * Show teams with no members if user is admin (https://github.com/go-gitea/gitea/pull/21204) + * Add latest commit's SHA to content response (https://github.com/go-gitea/gitea/pull/20398) + * Add allow_rebase_update, default_delete_branch_after_merge to repository api response (https://github.com/go-gitea/gitea/pull/20079) + * Add new endpoints for push mirrors management (https://github.com/go-gitea/gitea/pull/19841) +* ENHANCEMENTS + * Add setting to disable the git apply step in test patch (https://github.com/go-gitea/gitea/pull/22130) (https://github.com/go-gitea/gitea/pull/22170) + * Multiple improvements for comment edit diff (https://github.com/go-gitea/gitea/pull/21990) (https://github.com/go-gitea/gitea/pull/22007) + * Fix button in branch list, avoid unexpected page jump before restore branch actually done (https://github.com/go-gitea/gitea/pull/21562) (https://github.com/go-gitea/gitea/pull/21928) + * Fix flex layout for repo list icons (https://github.com/go-gitea/gitea/pull/21896) (https://github.com/go-gitea/gitea/pull/21920) + * Fix vertical align of committer avatar rendered by email address (https://github.com/go-gitea/gitea/pull/21884) (https://github.com/go-gitea/gitea/pull/21918) + * Fix setting HTTP headers after write (https://github.com/go-gitea/gitea/pull/21833) (https://github.com/go-gitea/gitea/pull/21877) + * Color and Style enhancements (https://github.com/go-gitea/gitea/pull/21784, #21799) (https://github.com/go-gitea/gitea/pull/21868) + * Ignore line anchor links with leading zeroes (https://github.com/go-gitea/gitea/pull/21728) (https://github.com/go-gitea/gitea/pull/21776) + * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (https://github.com/go-gitea/gitea/pull/21734) (https://github.com/go-gitea/gitea/pull/21738) + * Use CSS color-scheme instead of invert (https://github.com/go-gitea/gitea/pull/21616) (https://github.com/go-gitea/gitea/pull/21623) + * Respect user's locale when rendering the date range in the repo activity page (https://github.com/go-gitea/gitea/pull/21410) + * Change `commits-table` column width (https://github.com/go-gitea/gitea/pull/21564) + * Refactor git command arguments and make all arguments to be safe to be used (https://github.com/go-gitea/gitea/pull/21535) + * CSS color enhancements (https://github.com/go-gitea/gitea/pull/21534) + * Add link to user profile in markdown mention only if user exists (https://github.com/go-gitea/gitea/pull/21533, #21554) + * Add option to skip index dirs (https://github.com/go-gitea/gitea/pull/21501) + * Diff file tree tweaks (https://github.com/go-gitea/gitea/pull/21446) + * Localize all timestamps (https://github.com/go-gitea/gitea/pull/21440) + * Add `code` highlighting in issue titles (https://github.com/go-gitea/gitea/pull/21432) + * Use Name instead of DisplayName in LFS Lock (https://github.com/go-gitea/gitea/pull/21415) + * Consolidate more CSS colors into variables (https://github.com/go-gitea/gitea/pull/21402) + * Redirect to new repository owner (https://github.com/go-gitea/gitea/pull/21398) + * Use ISO date format instead of hard-coded English date format for date range in repo activity page (https://github.com/go-gitea/gitea/pull/21396) + * Use weighted algorithm for string matching when finding files in repo (https://github.com/go-gitea/gitea/pull/21370) + * Show private data in feeds (https://github.com/go-gitea/gitea/pull/21369) + * Refactor parseTreeEntries, speed up tree list (https://github.com/go-gitea/gitea/pull/21368) + * Add GET and DELETE endpoints for Docker blob uploads (https://github.com/go-gitea/gitea/pull/21367) + * Add nicer error handling on template compile errors (https://github.com/go-gitea/gitea/pull/21350) + * Add `stat` to `ToCommit` function for speed (https://github.com/go-gitea/gitea/pull/21337) + * Support instance-wide OAuth2 applications (https://github.com/go-gitea/gitea/pull/21335) + * Record OAuth client type at registration (https://github.com/go-gitea/gitea/pull/21316) + * Add new CSS variables --color-accent and --color-small-accent (https://github.com/go-gitea/gitea/pull/21305) + * Improve error descriptions for unauthorized_client (https://github.com/go-gitea/gitea/pull/21292) + * Case-insensitive "find files in repo" (https://github.com/go-gitea/gitea/pull/21269) + * Consolidate more CSS rules, fix inline code on arc-green (https://github.com/go-gitea/gitea/pull/21260) + * Log real ip of requests from ssh (https://github.com/go-gitea/gitea/pull/21216) + * Save files in local storage as group readable (https://github.com/go-gitea/gitea/pull/21198) + * Enable fluid page layout on medium size viewports (https://github.com/go-gitea/gitea/pull/21178) + * File header tweaks (https://github.com/go-gitea/gitea/pull/21175) + * Added missing headers on user packages page (https://github.com/go-gitea/gitea/pull/21172) + * Display image digest for container packages (https://github.com/go-gitea/gitea/pull/21170) + * Skip dirty check for team forms (https://github.com/go-gitea/gitea/pull/21154) + * Keep path when creating a new branch (https://github.com/go-gitea/gitea/pull/21153) + * Remove fomantic image module (https://github.com/go-gitea/gitea/pull/21145) + * Make labels clickable in the comments section. (https://github.com/go-gitea/gitea/pull/21137) + * Sort branches and tags by date descending (https://github.com/go-gitea/gitea/pull/21136) + * Better repo API unit checks (https://github.com/go-gitea/gitea/pull/21130) + * Improve commit status icons (https://github.com/go-gitea/gitea/pull/21124) + * Limit length of repo description and repo url input fields (https://github.com/go-gitea/gitea/pull/21119) + * Show .editorconfig errors in frontend (https://github.com/go-gitea/gitea/pull/21088) + * Allow poster to choose reviewers (https://github.com/go-gitea/gitea/pull/21084) + * Remove black labels and CSS cleanup (https://github.com/go-gitea/gitea/pull/21003) + * Make e-mail sanity check more precise (https://github.com/go-gitea/gitea/pull/20991) + * Use native inputs in whitespace dropdown (https://github.com/go-gitea/gitea/pull/20980) + * Enhance package date display (https://github.com/go-gitea/gitea/pull/20928) + * Display total blob size of a package version (https://github.com/go-gitea/gitea/pull/20927) + * Show language name on hover (https://github.com/go-gitea/gitea/pull/20923) + * Show instructions for all generic package files (https://github.com/go-gitea/gitea/pull/20917) + * Refactor AssertExistsAndLoadBean to use generics (https://github.com/go-gitea/gitea/pull/20797) + * Move the official website link at the footer of gitea (https://github.com/go-gitea/gitea/pull/20777) + * Add support for full name in reverse proxy auth (https://github.com/go-gitea/gitea/pull/20776) + * Remove useless JS operation for relative time tooltips (https://github.com/go-gitea/gitea/pull/20756) + * Replace some icons with SVG (https://github.com/go-gitea/gitea/pull/20741) + * Change commit status icons to SVG (https://github.com/go-gitea/gitea/pull/20736) + * Improve single repo action for issue and pull requests (https://github.com/go-gitea/gitea/pull/20730) + * Allow multiple files in generic packages (https://github.com/go-gitea/gitea/pull/20661) + * Add option to create new issue from /issues page (https://github.com/go-gitea/gitea/pull/20650) + * Background color of private list-items updated (https://github.com/go-gitea/gitea/pull/20630) + * Added search input field to issue filter (https://github.com/go-gitea/gitea/pull/20623) + * Increase default item listing size `ISSUE_PAGING_NUM` to 20 (https://github.com/go-gitea/gitea/pull/20547) + * Modify milestone search keywords to be case insensitive again (https://github.com/go-gitea/gitea/pull/20513) + * Show hint to link package to repo when viewing empty repo package list (https://github.com/go-gitea/gitea/pull/20504) + * Add Tar ZSTD support (https://github.com/go-gitea/gitea/pull/20493) + * Make code review checkboxes clickable (https://github.com/go-gitea/gitea/pull/20481) + * Add "X-Gitea-Object-Type" header for GET `/raw/` & `/media/` API (https://github.com/go-gitea/gitea/pull/20438) + * Display project in issue list (https://github.com/go-gitea/gitea/pull/20434) + * Prepend commit message to template content when opening a new PR (https://github.com/go-gitea/gitea/pull/20429) + * Replace fomantic popup module with tippy.js (https://github.com/go-gitea/gitea/pull/20428) + * Allow to specify colors for text in markup (https://github.com/go-gitea/gitea/pull/20363) + * Allow access to the Public Organization Member lists with minimal permissions (https://github.com/go-gitea/gitea/pull/20330) + * Use default values when provided values are empty (https://github.com/go-gitea/gitea/pull/20318) + * Vertical align navbar avatar at middle (https://github.com/go-gitea/gitea/pull/20302) + * Delete cancel button in repo creation page (https://github.com/go-gitea/gitea/pull/21381) + * Include login_name in adminCreateUser response (https://github.com/go-gitea/gitea/pull/20283) + * fix: icon margin in user/settings/repos (https://github.com/go-gitea/gitea/pull/20281) + * Remove blue text on migrate page (https://github.com/go-gitea/gitea/pull/20273) + * Modify milestone search keywords to be case insensitive (https://github.com/go-gitea/gitea/pull/20266) + * Move some files into models' sub packages (https://github.com/go-gitea/gitea/pull/20262) + * Add tooltip to repo icons in explore page (https://github.com/go-gitea/gitea/pull/20241) + * Remove deprecated licenses (https://github.com/go-gitea/gitea/pull/20222) + * Webhook for Wiki changes (https://github.com/go-gitea/gitea/pull/20219) + * Share HTML template renderers and create a watcher framework (https://github.com/go-gitea/gitea/pull/20218) + * Allow enable LDAP source and disable user sync via CLI (https://github.com/go-gitea/gitea/pull/20206) + * Adds a checkbox to select all issues/PRs (https://github.com/go-gitea/gitea/pull/20177) + * Refactor `i18n` to `locale` (https://github.com/go-gitea/gitea/pull/20153) + * Disable status checks in template if none found (https://github.com/go-gitea/gitea/pull/20088) + * Allow manager logging to set SQL (https://github.com/go-gitea/gitea/pull/20064) + * Add order by for assignee no sort issue (https://github.com/go-gitea/gitea/pull/20053) + * Take a stab at porting existing components to Vue3 (https://github.com/go-gitea/gitea/pull/20044) + * Add doctor command to write commit-graphs (https://github.com/go-gitea/gitea/pull/20007) + * Add support for authentication based on reverse proxy email (https://github.com/go-gitea/gitea/pull/19949) + * Enable spellcheck for EasyMDE, use contenteditable mode (https://github.com/go-gitea/gitea/pull/19776) + * Allow specifying SECRET_KEY_URI, similar to INTERNAL_TOKEN_URI (https://github.com/go-gitea/gitea/pull/19663) + * Rework mailer settings (https://github.com/go-gitea/gitea/pull/18982) + * Add option to purge users (https://github.com/go-gitea/gitea/pull/18064) + * Add author search input (https://github.com/go-gitea/gitea/pull/21246) + * Make rss/atom identifier globally unique (https://github.com/go-gitea/gitea/pull/21550) +* BUGFIXES + * Auth interface return error when verify failure (https://github.com/go-gitea/gitea/pull/22119) (https://github.com/go-gitea/gitea/pull/22259) + * Use complete SHA to create and query commit status (https://github.com/go-gitea/gitea/pull/22244) (https://github.com/go-gitea/gitea/pull/22257) + * Update bleve and zapx to fix unaligned atomic (https://github.com/go-gitea/gitea/pull/22031) (https://github.com/go-gitea/gitea/pull/22218) + * Prevent panic in doctor command when running default checks (https://github.com/go-gitea/gitea/pull/21791) (https://github.com/go-gitea/gitea/pull/21807) + * Load GitRepo in API before deleting issue (https://github.com/go-gitea/gitea/pull/21720) (https://github.com/go-gitea/gitea/pull/21796) + * Ignore line anchor links with leading zeroes (https://github.com/go-gitea/gitea/pull/21728) (https://github.com/go-gitea/gitea/pull/21776) + * Set last login when activating account (https://github.com/go-gitea/gitea/pull/21731) (https://github.com/go-gitea/gitea/pull/21755) + * Fix UI language switching bug (https://github.com/go-gitea/gitea/pull/21597) (https://github.com/go-gitea/gitea/pull/21749) + * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (https://github.com/go-gitea/gitea/pull/21734) (https://github.com/go-gitea/gitea/pull/21738) + * Allow local package identifiers for PyPI packages (https://github.com/go-gitea/gitea/pull/21690) (https://github.com/go-gitea/gitea/pull/21727) + * Deal with markdown template without metadata (https://github.com/go-gitea/gitea/pull/21639) (https://github.com/go-gitea/gitea/pull/21654) + * Fix opaque background on mermaid diagrams (https://github.com/go-gitea/gitea/pull/21642) (https://github.com/go-gitea/gitea/pull/21652) + * Fix repository adoption on Windows (https://github.com/go-gitea/gitea/pull/21646) (https://github.com/go-gitea/gitea/pull/21650) + * Sync git hooks when config file path changed (https://github.com/go-gitea/gitea/pull/21619) (https://github.com/go-gitea/gitea/pull/21626) + * Fix 500 on PR files API (https://github.com/go-gitea/gitea/pull/21602) (https://github.com/go-gitea/gitea/pull/21607) + * Fix `Timestamp.IsZero` (https://github.com/go-gitea/gitea/pull/21593) (https://github.com/go-gitea/gitea/pull/21603) + * Fix viewing user subscriptions (https://github.com/go-gitea/gitea/pull/21482) + * Fix mermaid-related bugs (https://github.com/go-gitea/gitea/pull/21431) + * Fix branch dropdown shifting on page load (https://github.com/go-gitea/gitea/pull/21428) + * Fix default theme-auto selector when nologin (https://github.com/go-gitea/gitea/pull/21346) + * Fix and improve incorrect error messages (https://github.com/go-gitea/gitea/pull/21342) + * Fix formatted link for PR review notifications to matrix (https://github.com/go-gitea/gitea/pull/21319) + * Center-aligning content of WebAuthN page (https://github.com/go-gitea/gitea/pull/21127) + * Remove follow from commits by file (https://github.com/go-gitea/gitea/pull/20765) + * Fix commit status popup (https://github.com/go-gitea/gitea/pull/20737) + * Fix init mail render logic (https://github.com/go-gitea/gitea/pull/20704) + * Use correct page size for link header pagination (https://github.com/go-gitea/gitea/pull/20546) + * Preserve unix socket file (https://github.com/go-gitea/gitea/pull/20499) + * Use tippy.js for context popup (https://github.com/go-gitea/gitea/pull/20393) + * Add missing parameter for error in log message (https://github.com/go-gitea/gitea/pull/20144) + * Do not allow organisation owners add themselves as collaborator (https://github.com/go-gitea/gitea/pull/20043) + * Rework file highlight rendering and fix yaml copy-paste (https://github.com/go-gitea/gitea/pull/19967) + * Improve code diff highlight, fix incorrect rendered diff result (https://github.com/go-gitea/gitea/pull/19958) +* TESTING + * Improve OAuth integration tests (https://github.com/go-gitea/gitea/pull/21390) + * Add playwright tests (https://github.com/go-gitea/gitea/pull/20123) +* BUILD + * Switch to building with go1.19 (https://github.com/go-gitea/gitea/pull/20695) + * Update JS dependencies, adjust eslint (https://github.com/go-gitea/gitea/pull/20659) + * Add more linters to improve code readability (https://github.com/go-gitea/gitea/pull/19989) + +## 1.18.0-0 + +This release was replaced by 1.18.0-1 a few hours after being published because the release process [was interrupted](https://codeberg.org/forgejo/forgejo/issues/180). + +## 1.18.0-rc1-2 + +This is the first Forgejo release candidate. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index ef98a2a8ac..0000000000 --- a/SECURITY.md +++ /dev/null @@ -1,83 +0,0 @@ -# Reporting security issues - -The Gitea maintainers take security seriously. - -If you discover a security issue, please bring it to their attention right away! - -## Reporting a Vulnerability - -Please **DO NOT** file a public issue, instead send your report privately to `security@gitea.io`. - -## Protecting Security Information - -Due to the sensitive nature of security information, you can use below GPG public key encrypt your mail body. - -The PGP key is valid until June 24, 2024. - -``` -Key ID: 6FCD2D5B -Key Type: RSA -Expires: 6/24/2024 -Key Size: 4096/4096 -Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B -``` - -UserID: Gitea Security - -``` ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBGK1Z/4BEADFMqXA9DeeChmSxUjF0Be5sq99ZUhgrZjcN/wOzz0wuCJZC0l8 -4uC+d6mfv7JpJYlzYzOK97/x5UguKHkYNZ6mm1G9KHaXmoIBDLKDzfPdJopVNv2r -OajijaE0uMCnMjadlg5pbhMLRQG8a9J32yyaz7ZEAw72Ab31fvvcA53NkuqO4j2w -k7dtFQzhbNOYV0VffQT90WDZdalYHB1JHyEQ+70U9OjVD5ggNYSzX98Eu3Hjn7V7 -kqFrcAxr5TE1elf0IXJcuBJtFzQSTUGlQldKOHtGTGgGjj9r/FFAE5ioBgVD05bV -rEEgIMM/GqYaG/nbNpWE6P3mEc2Mnn3pZaRJL0LuF26TLjnqEcMMDp5iIhLdFzXR -3tMdtKgQFu+Mtzs3ipwWARYgHyU09RJsI2HeBx7RmZO/Xqrec763Z7zdJ7SpCn0Z -q+pHZl24JYR0Kf3T/ZiOC0cGd2QJqpJtg5J6S/OqfX9NH6MsCczO8pUC1N/aHH2X -CTme2nF56izORqDWKoiICteL3GpYsCV9nyCidcCmoQsS+DKvE86YhIhVIVWGRY2F -lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e -AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB -tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBFiEE -PeA9HhRKfwaTWZncqv0jgW/NLVsFAmK1Z/4CGwMFCQPCZwAFCwkIBwICIgIGFQoJ -CAsCBBYCAwECHgcCF4AACgkQqv0jgW/NLVvnyxAAhxyNnWzw/rQO2qhzqicmZM94 -njSbOg+U2qMBvCdaqCQQeC+uaMmMzkDPanUUmLcyCkWqfCjPNjeSXAkE9npepVJI -4HtmgxZQ94OU/h3CLbft+9GVRzUkVI29TSYGdvNtV2/BkNGoFFnKWQr119um0o6A -bgha2Uy5uY8o3ZIoiKkiHRaEoWIjjeBxJxYAojsZY4YElUmsQ3ik2joG6rhFesTa -ofVt/bL8G2xzpOG26WGIxBbqf2qjV6OtZ0hu/vtTPHeIWMLq0Mz0V3PEDQWfkGPE -i2RYxxYDs2xzJhSQWqTNVLSq0m5xTJnbHhQPfdCX4C2jvFKgLdfmytQq49S7jiJb -Z03HVOZ/PsyBlQfH9xJi06R5yQCMEA8h8Z5r3/NXW09kQ6OFRe6xshoTcxZGRPTo -srhwr3uPbmCRh+YEl7qBLU6+BC5k8IRTZXqhrj/aPJu3MxgbgwV8u3vLoFSXM2lb -a61FgeCQ0O7lkgVswwF0RppCaH9Ul3ZDapet/vCRg4NVwm9zOI/8q/Vj0FKA1GDR -JhRu8+Ce8zlFL65D34t+PprAzSeTlbv9um3x/ZIjCco7EEKSBylt+AZj/VyA6+e5 -kjOQwRRc6dFJWBcorsSI2dG+H+QMF7ZabzmeCcz1v9HjLOPzYHoZAHhCmSppWTvX -AJy6+lhfW2OUTqQeYSi5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen -T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN -GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46 -XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E -ZDMkn/O6xnwWNzy/+bpg43qH/Gk0eakOmz5NmQLRkV58SZLiJvuCUtkttf6CyhnX -03OcWaajv5W8qA39dBYQgDrrPbBWUnwfO3yMveqhwV4JjDoe8sPAyn1NwzakNYqP -RzsWyLrLS7R7J9s3FkZXhQw/QQcsaSMcGNQO047dm1P83N8JY5aEpiRo9zSWjoiw -qoExANj5lUTZPe8M50lI182FrcjAN7dClO3QI6pg7wy0erMxfFly3j8UQ91ysS9T -s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/ -axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt -D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x -Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYWIQQ94D0eFEp/BpNZmdyq/SOBb80tWwUC -YrVn/gIbDAUJA8JnAAAKCRCq/SOBb80tWyTBD/9AGpW6QoDF7zYjHAozH9S5RGCA -Y7E82dG/0xmFUwPprAG0BKmmgU6TiipyVGmKIXGYYYU92pMnbvXkYQMoa+WJNncN -D3fY52UeXeffTf4cFpStlzi9xgYtOLhFamzYu/4xhkjOX+xhOSXscCiFRyT8cF3B -O6c5BHU+Zj0/rGPgOyPUbx7l7B9MubB/41nNX35k08e+8T3wtWDb4XF+15HnRfva -6fblO8wgU25Orv2Rm1jnKGa9DxJ8nE40IMrqDapENtDuL+zKJsvR0+ptWvEyL56U -GtJJG5un6mXiLKuRQT0DEv4MdZRHDgDstDnqcbEiazVEbUuvhZZob6lRY2A19m1+ -7zfnDxkhqCA1RCnv4fdvcPdCMMFHwLpdhjgW0aI/uwgwrvsEz5+JRlnLvdQHlPAg -q7l2fGcBSpz9U0ayyfRPjPntsNCtZl1UDxGLeciPkZhyG84zEWQbk/j52ZpRN+Ik -ALpRLa8RBFmFSmXDUmwQrmm1EmARyQXwweKU31hf8ZGbCp2lPuRYm1LuGiirXSVP -GysjRAJgW+VRpBKOzFQoUAUbReVWSaCwT8s17THzf71DdDb6CTj31jMLLYWwBpA/ -i73DgobDZMIGEZZC1EKqza8eh11xfyHFzGec03tbh+lIen+5IiRtWiEWkDS9ll0G -zgS/ZdziCvdAutqnGA== -=gZWO ------END PGP PUBLIC KEY BLOCK----- - -``` - -Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it. diff --git a/assets/emoji.json b/assets/emoji.json index bf5f1de60f..5a1ff98a46 100644 --- a/assets/emoji.json +++ b/assets/emoji.json @@ -1 +1 @@ -[{"emoji":"👍","aliases":["+1","thumbsup"]},{"emoji":"👎","aliases":["-1","thumbsdown"]},{"emoji":"💯","aliases":["100"]},{"emoji":"🔢","aliases":["1234"]},{"emoji":"🥇","aliases":["1st_place_medal"]},{"emoji":"🥈","aliases":["2nd_place_medal"]},{"emoji":"🥉","aliases":["3rd_place_medal"]},{"emoji":"🎱","aliases":["8ball"]},{"emoji":"🅰️","aliases":["a"]},{"emoji":"🆎","aliases":["ab"]},{"emoji":"🧮","aliases":["abacus"]},{"emoji":"🔤","aliases":["abc"]},{"emoji":"🔡","aliases":["abcd"]},{"emoji":"🉑","aliases":["accept"]},{"emoji":"🩹","aliases":["adhesive_bandage"]},{"emoji":"🧑","aliases":["adult"]},{"emoji":"🚡","aliases":["aerial_tramway"]},{"emoji":"🇦🇫","aliases":["afghanistan"]},{"emoji":"✈️","aliases":["airplane"]},{"emoji":"🇦🇽","aliases":["aland_islands"]},{"emoji":"⏰","aliases":["alarm_clock"]},{"emoji":"🇦🇱","aliases":["albania"]},{"emoji":"⚗️","aliases":["alembic"]},{"emoji":"🇩🇿","aliases":["algeria"]},{"emoji":"👽","aliases":["alien"]},{"emoji":"🚑","aliases":["ambulance"]},{"emoji":"🇦🇸","aliases":["american_samoa"]},{"emoji":"🏺","aliases":["amphora"]},{"emoji":"⚓","aliases":["anchor"]},{"emoji":"🇦🇩","aliases":["andorra"]},{"emoji":"👼","aliases":["angel"]},{"emoji":"💢","aliases":["anger"]},{"emoji":"🇦🇴","aliases":["angola"]},{"emoji":"😠","aliases":["angry"]},{"emoji":"🇦🇮","aliases":["anguilla"]},{"emoji":"😧","aliases":["anguished"]},{"emoji":"🐜","aliases":["ant"]},{"emoji":"🇦🇶","aliases":["antarctica"]},{"emoji":"🇦🇬","aliases":["antigua_barbuda"]},{"emoji":"🍎","aliases":["apple"]},{"emoji":"♒","aliases":["aquarius"]},{"emoji":"🇦🇷","aliases":["argentina"]},{"emoji":"♈","aliases":["aries"]},{"emoji":"🇦🇲","aliases":["armenia"]},{"emoji":"◀️","aliases":["arrow_backward"]},{"emoji":"⏬","aliases":["arrow_double_down"]},{"emoji":"⏫","aliases":["arrow_double_up"]},{"emoji":"⬇️","aliases":["arrow_down"]},{"emoji":"🔽","aliases":["arrow_down_small"]},{"emoji":"▶️","aliases":["arrow_forward"]},{"emoji":"⤵️","aliases":["arrow_heading_down"]},{"emoji":"⤴️","aliases":["arrow_heading_up"]},{"emoji":"⬅️","aliases":["arrow_left"]},{"emoji":"↙️","aliases":["arrow_lower_left"]},{"emoji":"↘️","aliases":["arrow_lower_right"]},{"emoji":"➡️","aliases":["arrow_right"]},{"emoji":"↪️","aliases":["arrow_right_hook"]},{"emoji":"⬆️","aliases":["arrow_up"]},{"emoji":"↕️","aliases":["arrow_up_down"]},{"emoji":"🔼","aliases":["arrow_up_small"]},{"emoji":"↖️","aliases":["arrow_upper_left"]},{"emoji":"↗️","aliases":["arrow_upper_right"]},{"emoji":"🔃","aliases":["arrows_clockwise"]},{"emoji":"🔄","aliases":["arrows_counterclockwise"]},{"emoji":"🎨","aliases":["art"]},{"emoji":"🚛","aliases":["articulated_lorry"]},{"emoji":"🛰️","aliases":["artificial_satellite"]},{"emoji":"🧑‍🎨","aliases":["artist"]},{"emoji":"🇦🇼","aliases":["aruba"]},{"emoji":"🇦🇨","aliases":["ascension_island"]},{"emoji":"*️⃣","aliases":["asterisk"]},{"emoji":"😲","aliases":["astonished"]},{"emoji":"🧑‍🚀","aliases":["astronaut"]},{"emoji":"👟","aliases":["athletic_shoe"]},{"emoji":"🏧","aliases":["atm"]},{"emoji":"⚛️","aliases":["atom_symbol"]},{"emoji":"🇦🇺","aliases":["australia"]},{"emoji":"🇦🇹","aliases":["austria"]},{"emoji":"🛺","aliases":["auto_rickshaw"]},{"emoji":"🥑","aliases":["avocado"]},{"emoji":"🪓","aliases":["axe"]},{"emoji":"🇦🇿","aliases":["azerbaijan"]},{"emoji":"🅱️","aliases":["b"]},{"emoji":"👶","aliases":["baby"]},{"emoji":"🍼","aliases":["baby_bottle"]},{"emoji":"🐤","aliases":["baby_chick"]},{"emoji":"🚼","aliases":["baby_symbol"]},{"emoji":"🔙","aliases":["back"]},{"emoji":"🥓","aliases":["bacon"]},{"emoji":"🦡","aliases":["badger"]},{"emoji":"🏸","aliases":["badminton"]},{"emoji":"🥯","aliases":["bagel"]},{"emoji":"🛄","aliases":["baggage_claim"]},{"emoji":"🥖","aliases":["baguette_bread"]},{"emoji":"🇧🇸","aliases":["bahamas"]},{"emoji":"🇧🇭","aliases":["bahrain"]},{"emoji":"⚖️","aliases":["balance_scale"]},{"emoji":"👨‍🦲","aliases":["bald_man"]},{"emoji":"👩‍🦲","aliases":["bald_woman"]},{"emoji":"🩰","aliases":["ballet_shoes"]},{"emoji":"🎈","aliases":["balloon"]},{"emoji":"🗳️","aliases":["ballot_box"]},{"emoji":"☑️","aliases":["ballot_box_with_check"]},{"emoji":"🎍","aliases":["bamboo"]},{"emoji":"🍌","aliases":["banana"]},{"emoji":"‼️","aliases":["bangbang"]},{"emoji":"🇧🇩","aliases":["bangladesh"]},{"emoji":"🪕","aliases":["banjo"]},{"emoji":"🏦","aliases":["bank"]},{"emoji":"📊","aliases":["bar_chart"]},{"emoji":"🇧🇧","aliases":["barbados"]},{"emoji":"💈","aliases":["barber"]},{"emoji":"⚾","aliases":["baseball"]},{"emoji":"🧺","aliases":["basket"]},{"emoji":"🏀","aliases":["basketball"]},{"emoji":"🦇","aliases":["bat"]},{"emoji":"🛀","aliases":["bath"]},{"emoji":"🛁","aliases":["bathtub"]},{"emoji":"🔋","aliases":["battery"]},{"emoji":"🏖️","aliases":["beach_umbrella"]},{"emoji":"🐻","aliases":["bear"]},{"emoji":"🧔","aliases":["bearded_person"]},{"emoji":"🛏️","aliases":["bed"]},{"emoji":"🐝","aliases":["bee","honeybee"]},{"emoji":"🍺","aliases":["beer"]},{"emoji":"🍻","aliases":["beers"]},{"emoji":"🔰","aliases":["beginner"]},{"emoji":"🇧🇾","aliases":["belarus"]},{"emoji":"🇧🇪","aliases":["belgium"]},{"emoji":"🇧🇿","aliases":["belize"]},{"emoji":"🔔","aliases":["bell"]},{"emoji":"🛎️","aliases":["bellhop_bell"]},{"emoji":"🇧🇯","aliases":["benin"]},{"emoji":"🍱","aliases":["bento"]},{"emoji":"🇧🇲","aliases":["bermuda"]},{"emoji":"🧃","aliases":["beverage_box"]},{"emoji":"🇧🇹","aliases":["bhutan"]},{"emoji":"🚴","aliases":["bicyclist"]},{"emoji":"🚲","aliases":["bike"]},{"emoji":"🚴‍♂️","aliases":["biking_man"]},{"emoji":"🚴‍♀️","aliases":["biking_woman"]},{"emoji":"👙","aliases":["bikini"]},{"emoji":"🧢","aliases":["billed_cap"]},{"emoji":"☣️","aliases":["biohazard"]},{"emoji":"🐦","aliases":["bird"]},{"emoji":"🎂","aliases":["birthday"]},{"emoji":"⚫","aliases":["black_circle"]},{"emoji":"🏴","aliases":["black_flag"]},{"emoji":"🖤","aliases":["black_heart"]},{"emoji":"🃏","aliases":["black_joker"]},{"emoji":"⬛","aliases":["black_large_square"]},{"emoji":"◾","aliases":["black_medium_small_square"]},{"emoji":"◼️","aliases":["black_medium_square"]},{"emoji":"✒️","aliases":["black_nib"]},{"emoji":"▪️","aliases":["black_small_square"]},{"emoji":"🔲","aliases":["black_square_button"]},{"emoji":"👱‍♂️","aliases":["blond_haired_man"]},{"emoji":"👱","aliases":["blond_haired_person"]},{"emoji":"👱‍♀️","aliases":["blond_haired_woman","blonde_woman"]},{"emoji":"🌼","aliases":["blossom"]},{"emoji":"🐡","aliases":["blowfish"]},{"emoji":"📘","aliases":["blue_book"]},{"emoji":"🚙","aliases":["blue_car"]},{"emoji":"💙","aliases":["blue_heart"]},{"emoji":"🟦","aliases":["blue_square"]},{"emoji":"😊","aliases":["blush"]},{"emoji":"🐗","aliases":["boar"]},{"emoji":"⛵","aliases":["boat","sailboat"]},{"emoji":"🇧🇴","aliases":["bolivia"]},{"emoji":"💣","aliases":["bomb"]},{"emoji":"🦴","aliases":["bone"]},{"emoji":"📖","aliases":["book","open_book"]},{"emoji":"🔖","aliases":["bookmark"]},{"emoji":"📑","aliases":["bookmark_tabs"]},{"emoji":"📚","aliases":["books"]},{"emoji":"💥","aliases":["boom","collision"]},{"emoji":"👢","aliases":["boot"]},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"]},{"emoji":"🇧🇼","aliases":["botswana"]},{"emoji":"⛹️‍♂️","aliases":["bouncing_ball_man","basketball_man"]},{"emoji":"⛹️","aliases":["bouncing_ball_person"]},{"emoji":"⛹️‍♀️","aliases":["bouncing_ball_woman","basketball_woman"]},{"emoji":"💐","aliases":["bouquet"]},{"emoji":"🇧🇻","aliases":["bouvet_island"]},{"emoji":"🙇","aliases":["bow"]},{"emoji":"🏹","aliases":["bow_and_arrow"]},{"emoji":"🙇‍♂️","aliases":["bowing_man"]},{"emoji":"🙇‍♀️","aliases":["bowing_woman"]},{"emoji":"🥣","aliases":["bowl_with_spoon"]},{"emoji":"🎳","aliases":["bowling"]},{"emoji":"🥊","aliases":["boxing_glove"]},{"emoji":"👦","aliases":["boy"]},{"emoji":"🧠","aliases":["brain"]},{"emoji":"🇧🇷","aliases":["brazil"]},{"emoji":"🍞","aliases":["bread"]},{"emoji":"🤱","aliases":["breast_feeding"]},{"emoji":"🧱","aliases":["bricks"]},{"emoji":"🌉","aliases":["bridge_at_night"]},{"emoji":"💼","aliases":["briefcase"]},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"]},{"emoji":"🇻🇬","aliases":["british_virgin_islands"]},{"emoji":"🥦","aliases":["broccoli"]},{"emoji":"💔","aliases":["broken_heart"]},{"emoji":"🧹","aliases":["broom"]},{"emoji":"🟤","aliases":["brown_circle"]},{"emoji":"🤎","aliases":["brown_heart"]},{"emoji":"🟫","aliases":["brown_square"]},{"emoji":"🇧🇳","aliases":["brunei"]},{"emoji":"🐛","aliases":["bug"]},{"emoji":"🏗️","aliases":["building_construction"]},{"emoji":"💡","aliases":["bulb"]},{"emoji":"🇧🇬","aliases":["bulgaria"]},{"emoji":"🚅","aliases":["bullettrain_front"]},{"emoji":"🚄","aliases":["bullettrain_side"]},{"emoji":"🇧🇫","aliases":["burkina_faso"]},{"emoji":"🌯","aliases":["burrito"]},{"emoji":"🇧🇮","aliases":["burundi"]},{"emoji":"🚌","aliases":["bus"]},{"emoji":"🕴️","aliases":["business_suit_levitating"]},{"emoji":"🚏","aliases":["busstop"]},{"emoji":"👤","aliases":["bust_in_silhouette"]},{"emoji":"👥","aliases":["busts_in_silhouette"]},{"emoji":"🧈","aliases":["butter"]},{"emoji":"🦋","aliases":["butterfly"]},{"emoji":"🌵","aliases":["cactus"]},{"emoji":"🍰","aliases":["cake"]},{"emoji":"📆","aliases":["calendar"]},{"emoji":"🤙","aliases":["call_me_hand"]},{"emoji":"📲","aliases":["calling"]},{"emoji":"🇰🇭","aliases":["cambodia"]},{"emoji":"🐫","aliases":["camel"]},{"emoji":"📷","aliases":["camera"]},{"emoji":"📸","aliases":["camera_flash"]},{"emoji":"🇨🇲","aliases":["cameroon"]},{"emoji":"🏕️","aliases":["camping"]},{"emoji":"🇨🇦","aliases":["canada"]},{"emoji":"🇮🇨","aliases":["canary_islands"]},{"emoji":"♋","aliases":["cancer"]},{"emoji":"🕯️","aliases":["candle"]},{"emoji":"🍬","aliases":["candy"]},{"emoji":"🥫","aliases":["canned_food"]},{"emoji":"🛶","aliases":["canoe"]},{"emoji":"🇨🇻","aliases":["cape_verde"]},{"emoji":"🔠","aliases":["capital_abcd"]},{"emoji":"♑","aliases":["capricorn"]},{"emoji":"🚗","aliases":["car","red_car"]},{"emoji":"🗃️","aliases":["card_file_box"]},{"emoji":"📇","aliases":["card_index"]},{"emoji":"🗂️","aliases":["card_index_dividers"]},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"]},{"emoji":"🎠","aliases":["carousel_horse"]},{"emoji":"🥕","aliases":["carrot"]},{"emoji":"🤸","aliases":["cartwheeling"]},{"emoji":"🐱","aliases":["cat"]},{"emoji":"🐈","aliases":["cat2"]},{"emoji":"🇰🇾","aliases":["cayman_islands"]},{"emoji":"💿","aliases":["cd"]},{"emoji":"🇨🇫","aliases":["central_african_republic"]},{"emoji":"🇪🇦","aliases":["ceuta_melilla"]},{"emoji":"🇹🇩","aliases":["chad"]},{"emoji":"⛓️","aliases":["chains"]},{"emoji":"🪑","aliases":["chair"]},{"emoji":"🍾","aliases":["champagne"]},{"emoji":"💹","aliases":["chart"]},{"emoji":"📉","aliases":["chart_with_downwards_trend"]},{"emoji":"📈","aliases":["chart_with_upwards_trend"]},{"emoji":"🏁","aliases":["checkered_flag"]},{"emoji":"🧀","aliases":["cheese"]},{"emoji":"🍒","aliases":["cherries"]},{"emoji":"🌸","aliases":["cherry_blossom"]},{"emoji":"♟️","aliases":["chess_pawn"]},{"emoji":"🌰","aliases":["chestnut"]},{"emoji":"🐔","aliases":["chicken"]},{"emoji":"🧒","aliases":["child"]},{"emoji":"🚸","aliases":["children_crossing"]},{"emoji":"🇨🇱","aliases":["chile"]},{"emoji":"🐿️","aliases":["chipmunk"]},{"emoji":"🍫","aliases":["chocolate_bar"]},{"emoji":"🥢","aliases":["chopsticks"]},{"emoji":"🇨🇽","aliases":["christmas_island"]},{"emoji":"🎄","aliases":["christmas_tree"]},{"emoji":"⛪","aliases":["church"]},{"emoji":"🎦","aliases":["cinema"]},{"emoji":"🎪","aliases":["circus_tent"]},{"emoji":"🌇","aliases":["city_sunrise"]},{"emoji":"🌆","aliases":["city_sunset"]},{"emoji":"🏙️","aliases":["cityscape"]},{"emoji":"🆑","aliases":["cl"]},{"emoji":"🗜️","aliases":["clamp"]},{"emoji":"👏","aliases":["clap"]},{"emoji":"🎬","aliases":["clapper"]},{"emoji":"🏛️","aliases":["classical_building"]},{"emoji":"🧗","aliases":["climbing"]},{"emoji":"🧗‍♂️","aliases":["climbing_man"]},{"emoji":"🧗‍♀️","aliases":["climbing_woman"]},{"emoji":"🥂","aliases":["clinking_glasses"]},{"emoji":"📋","aliases":["clipboard"]},{"emoji":"🇨🇵","aliases":["clipperton_island"]},{"emoji":"🕐","aliases":["clock1"]},{"emoji":"🕙","aliases":["clock10"]},{"emoji":"🕥","aliases":["clock1030"]},{"emoji":"🕚","aliases":["clock11"]},{"emoji":"🕦","aliases":["clock1130"]},{"emoji":"🕛","aliases":["clock12"]},{"emoji":"🕧","aliases":["clock1230"]},{"emoji":"🕜","aliases":["clock130"]},{"emoji":"🕑","aliases":["clock2"]},{"emoji":"🕝","aliases":["clock230"]},{"emoji":"🕒","aliases":["clock3"]},{"emoji":"🕞","aliases":["clock330"]},{"emoji":"🕓","aliases":["clock4"]},{"emoji":"🕟","aliases":["clock430"]},{"emoji":"🕔","aliases":["clock5"]},{"emoji":"🕠","aliases":["clock530"]},{"emoji":"🕕","aliases":["clock6"]},{"emoji":"🕡","aliases":["clock630"]},{"emoji":"🕖","aliases":["clock7"]},{"emoji":"🕢","aliases":["clock730"]},{"emoji":"🕗","aliases":["clock8"]},{"emoji":"🕣","aliases":["clock830"]},{"emoji":"🕘","aliases":["clock9"]},{"emoji":"🕤","aliases":["clock930"]},{"emoji":"📕","aliases":["closed_book"]},{"emoji":"🔐","aliases":["closed_lock_with_key"]},{"emoji":"🌂","aliases":["closed_umbrella"]},{"emoji":"☁️","aliases":["cloud"]},{"emoji":"🌩️","aliases":["cloud_with_lightning"]},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"]},{"emoji":"🌧️","aliases":["cloud_with_rain"]},{"emoji":"🌨️","aliases":["cloud_with_snow"]},{"emoji":"🤡","aliases":["clown_face"]},{"emoji":"♣️","aliases":["clubs"]},{"emoji":"🇨🇳","aliases":["cn"]},{"emoji":"🧥","aliases":["coat"]},{"emoji":"🍸","aliases":["cocktail"]},{"emoji":"🥥","aliases":["coconut"]},{"emoji":"🇨🇨","aliases":["cocos_islands"]},{"emoji":"☕","aliases":["coffee"]},{"emoji":"⚰️","aliases":["coffin"]},{"emoji":"🥶","aliases":["cold_face"]},{"emoji":"😰","aliases":["cold_sweat"]},{"emoji":"🇨🇴","aliases":["colombia"]},{"emoji":"☄️","aliases":["comet"]},{"emoji":"🇰🇲","aliases":["comoros"]},{"emoji":"🧭","aliases":["compass"]},{"emoji":"💻","aliases":["computer"]},{"emoji":"🖱️","aliases":["computer_mouse"]},{"emoji":"🎊","aliases":["confetti_ball"]},{"emoji":"😖","aliases":["confounded"]},{"emoji":"😕","aliases":["confused"]},{"emoji":"🇨🇬","aliases":["congo_brazzaville"]},{"emoji":"🇨🇩","aliases":["congo_kinshasa"]},{"emoji":"㊗️","aliases":["congratulations"]},{"emoji":"🚧","aliases":["construction"]},{"emoji":"👷","aliases":["construction_worker"]},{"emoji":"👷‍♂️","aliases":["construction_worker_man"]},{"emoji":"👷‍♀️","aliases":["construction_worker_woman"]},{"emoji":"🎛️","aliases":["control_knobs"]},{"emoji":"🏪","aliases":["convenience_store"]},{"emoji":"🧑‍🍳","aliases":["cook"]},{"emoji":"🇨🇰","aliases":["cook_islands"]},{"emoji":"🍪","aliases":["cookie"]},{"emoji":"🆒","aliases":["cool"]},{"emoji":"©️","aliases":["copyright"]},{"emoji":"🌽","aliases":["corn"]},{"emoji":"🇨🇷","aliases":["costa_rica"]},{"emoji":"🇨🇮","aliases":["cote_divoire"]},{"emoji":"🛋️","aliases":["couch_and_lamp"]},{"emoji":"👫","aliases":["couple"]},{"emoji":"💑","aliases":["couple_with_heart"]},{"emoji":"👨‍❤️‍👨","aliases":["couple_with_heart_man_man"]},{"emoji":"👩‍❤️‍👨","aliases":["couple_with_heart_woman_man"]},{"emoji":"👩‍❤️‍👩","aliases":["couple_with_heart_woman_woman"]},{"emoji":"💏","aliases":["couplekiss"]},{"emoji":"👨‍❤️‍💋‍👨","aliases":["couplekiss_man_man"]},{"emoji":"👩‍❤️‍💋‍👨","aliases":["couplekiss_man_woman"]},{"emoji":"👩‍❤️‍💋‍👩","aliases":["couplekiss_woman_woman"]},{"emoji":"🐮","aliases":["cow"]},{"emoji":"🐄","aliases":["cow2"]},{"emoji":"🤠","aliases":["cowboy_hat_face"]},{"emoji":"🦀","aliases":["crab"]},{"emoji":"🖍️","aliases":["crayon"]},{"emoji":"💳","aliases":["credit_card"]},{"emoji":"🌙","aliases":["crescent_moon"]},{"emoji":"🦗","aliases":["cricket"]},{"emoji":"🏏","aliases":["cricket_game"]},{"emoji":"🇭🇷","aliases":["croatia"]},{"emoji":"🐊","aliases":["crocodile"]},{"emoji":"🥐","aliases":["croissant"]},{"emoji":"🤞","aliases":["crossed_fingers"]},{"emoji":"🎌","aliases":["crossed_flags"]},{"emoji":"⚔️","aliases":["crossed_swords"]},{"emoji":"👑","aliases":["crown"]},{"emoji":"😢","aliases":["cry"]},{"emoji":"😿","aliases":["crying_cat_face"]},{"emoji":"🔮","aliases":["crystal_ball"]},{"emoji":"🇨🇺","aliases":["cuba"]},{"emoji":"🥒","aliases":["cucumber"]},{"emoji":"🥤","aliases":["cup_with_straw"]},{"emoji":"🧁","aliases":["cupcake"]},{"emoji":"💘","aliases":["cupid"]},{"emoji":"🇨🇼","aliases":["curacao"]},{"emoji":"🥌","aliases":["curling_stone"]},{"emoji":"👨‍🦱","aliases":["curly_haired_man"]},{"emoji":"👩‍🦱","aliases":["curly_haired_woman"]},{"emoji":"➰","aliases":["curly_loop"]},{"emoji":"💱","aliases":["currency_exchange"]},{"emoji":"🍛","aliases":["curry"]},{"emoji":"🤬","aliases":["cursing_face"]},{"emoji":"🍮","aliases":["custard"]},{"emoji":"🛃","aliases":["customs"]},{"emoji":"🥩","aliases":["cut_of_meat"]},{"emoji":"🌀","aliases":["cyclone"]},{"emoji":"🇨🇾","aliases":["cyprus"]},{"emoji":"🇨🇿","aliases":["czech_republic"]},{"emoji":"🗡️","aliases":["dagger"]},{"emoji":"👯","aliases":["dancers"]},{"emoji":"👯‍♂️","aliases":["dancing_men"]},{"emoji":"👯‍♀️","aliases":["dancing_women"]},{"emoji":"🍡","aliases":["dango"]},{"emoji":"🕶️","aliases":["dark_sunglasses"]},{"emoji":"🎯","aliases":["dart"]},{"emoji":"💨","aliases":["dash"]},{"emoji":"📅","aliases":["date"]},{"emoji":"🇩🇪","aliases":["de"]},{"emoji":"🧏‍♂️","aliases":["deaf_man"]},{"emoji":"🧏","aliases":["deaf_person"]},{"emoji":"🧏‍♀️","aliases":["deaf_woman"]},{"emoji":"🌳","aliases":["deciduous_tree"]},{"emoji":"🦌","aliases":["deer"]},{"emoji":"🇩🇰","aliases":["denmark"]},{"emoji":"🏬","aliases":["department_store"]},{"emoji":"🏚️","aliases":["derelict_house"]},{"emoji":"🏜️","aliases":["desert"]},{"emoji":"🏝️","aliases":["desert_island"]},{"emoji":"🖥️","aliases":["desktop_computer"]},{"emoji":"🕵️","aliases":["detective"]},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"]},{"emoji":"♦️","aliases":["diamonds"]},{"emoji":"🇩🇬","aliases":["diego_garcia"]},{"emoji":"😞","aliases":["disappointed"]},{"emoji":"😥","aliases":["disappointed_relieved"]},{"emoji":"🤿","aliases":["diving_mask"]},{"emoji":"🪔","aliases":["diya_lamp"]},{"emoji":"💫","aliases":["dizzy"]},{"emoji":"😵","aliases":["dizzy_face"]},{"emoji":"🇩🇯","aliases":["djibouti"]},{"emoji":"🧬","aliases":["dna"]},{"emoji":"🚯","aliases":["do_not_litter"]},{"emoji":"🐶","aliases":["dog"]},{"emoji":"🐕","aliases":["dog2"]},{"emoji":"💵","aliases":["dollar"]},{"emoji":"🎎","aliases":["dolls"]},{"emoji":"🐬","aliases":["dolphin","flipper"]},{"emoji":"🇩🇲","aliases":["dominica"]},{"emoji":"🇩🇴","aliases":["dominican_republic"]},{"emoji":"🚪","aliases":["door"]},{"emoji":"🍩","aliases":["doughnut"]},{"emoji":"🕊️","aliases":["dove"]},{"emoji":"🐉","aliases":["dragon"]},{"emoji":"🐲","aliases":["dragon_face"]},{"emoji":"👗","aliases":["dress"]},{"emoji":"🐪","aliases":["dromedary_camel"]},{"emoji":"🤤","aliases":["drooling_face"]},{"emoji":"🩸","aliases":["drop_of_blood"]},{"emoji":"💧","aliases":["droplet"]},{"emoji":"🥁","aliases":["drum"]},{"emoji":"🦆","aliases":["duck"]},{"emoji":"🥟","aliases":["dumpling"]},{"emoji":"📀","aliases":["dvd"]},{"emoji":"🦅","aliases":["eagle"]},{"emoji":"👂","aliases":["ear"]},{"emoji":"🌾","aliases":["ear_of_rice"]},{"emoji":"🦻","aliases":["ear_with_hearing_aid"]},{"emoji":"🌍","aliases":["earth_africa"]},{"emoji":"🌎","aliases":["earth_americas"]},{"emoji":"🌏","aliases":["earth_asia"]},{"emoji":"🇪🇨","aliases":["ecuador"]},{"emoji":"🥚","aliases":["egg"]},{"emoji":"🍆","aliases":["eggplant"]},{"emoji":"🇪🇬","aliases":["egypt"]},{"emoji":"8️⃣","aliases":["eight"]},{"emoji":"✴️","aliases":["eight_pointed_black_star"]},{"emoji":"✳️","aliases":["eight_spoked_asterisk"]},{"emoji":"⏏️","aliases":["eject_button"]},{"emoji":"🇸🇻","aliases":["el_salvador"]},{"emoji":"🔌","aliases":["electric_plug"]},{"emoji":"🐘","aliases":["elephant"]},{"emoji":"🧝","aliases":["elf"]},{"emoji":"🧝‍♂️","aliases":["elf_man"]},{"emoji":"🧝‍♀️","aliases":["elf_woman"]},{"emoji":"📧","aliases":["email","e-mail"]},{"emoji":"🔚","aliases":["end"]},{"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","aliases":["england"]},{"emoji":"✉️","aliases":["envelope"]},{"emoji":"📩","aliases":["envelope_with_arrow"]},{"emoji":"🇬🇶","aliases":["equatorial_guinea"]},{"emoji":"🇪🇷","aliases":["eritrea"]},{"emoji":"🇪🇸","aliases":["es"]},{"emoji":"🇪🇪","aliases":["estonia"]},{"emoji":"🇪🇹","aliases":["ethiopia"]},{"emoji":"🇪🇺","aliases":["eu","european_union"]},{"emoji":"💶","aliases":["euro"]},{"emoji":"🏰","aliases":["european_castle"]},{"emoji":"🏤","aliases":["european_post_office"]},{"emoji":"🌲","aliases":["evergreen_tree"]},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"]},{"emoji":"🤯","aliases":["exploding_head"]},{"emoji":"😑","aliases":["expressionless"]},{"emoji":"👁️","aliases":["eye"]},{"emoji":"👁️‍🗨️","aliases":["eye_speech_bubble"]},{"emoji":"👓","aliases":["eyeglasses"]},{"emoji":"👀","aliases":["eyes"]},{"emoji":"🤕","aliases":["face_with_head_bandage"]},{"emoji":"🤒","aliases":["face_with_thermometer"]},{"emoji":"🤦","aliases":["facepalm"]},{"emoji":"🏭","aliases":["factory"]},{"emoji":"🧑‍🏭","aliases":["factory_worker"]},{"emoji":"🧚","aliases":["fairy"]},{"emoji":"🧚‍♂️","aliases":["fairy_man"]},{"emoji":"🧚‍♀️","aliases":["fairy_woman"]},{"emoji":"🧆","aliases":["falafel"]},{"emoji":"🇫🇰","aliases":["falkland_islands"]},{"emoji":"🍂","aliases":["fallen_leaf"]},{"emoji":"👪","aliases":["family"]},{"emoji":"👨‍👦","aliases":["family_man_boy"]},{"emoji":"👨‍👦‍👦","aliases":["family_man_boy_boy"]},{"emoji":"👨‍👧","aliases":["family_man_girl"]},{"emoji":"👨‍👧‍👦","aliases":["family_man_girl_boy"]},{"emoji":"👨‍👧‍👧","aliases":["family_man_girl_girl"]},{"emoji":"👨‍👨‍👦","aliases":["family_man_man_boy"]},{"emoji":"👨‍👨‍👦‍👦","aliases":["family_man_man_boy_boy"]},{"emoji":"👨‍👨‍👧","aliases":["family_man_man_girl"]},{"emoji":"👨‍👨‍👧‍👦","aliases":["family_man_man_girl_boy"]},{"emoji":"👨‍👨‍👧‍👧","aliases":["family_man_man_girl_girl"]},{"emoji":"👨‍👩‍👦","aliases":["family_man_woman_boy"]},{"emoji":"👨‍👩‍👦‍👦","aliases":["family_man_woman_boy_boy"]},{"emoji":"👨‍👩‍👧","aliases":["family_man_woman_girl"]},{"emoji":"👨‍👩‍👧‍👦","aliases":["family_man_woman_girl_boy"]},{"emoji":"👨‍👩‍👧‍👧","aliases":["family_man_woman_girl_girl"]},{"emoji":"👩‍👦","aliases":["family_woman_boy"]},{"emoji":"👩‍👦‍👦","aliases":["family_woman_boy_boy"]},{"emoji":"👩‍👧","aliases":["family_woman_girl"]},{"emoji":"👩‍👧‍👦","aliases":["family_woman_girl_boy"]},{"emoji":"👩‍👧‍👧","aliases":["family_woman_girl_girl"]},{"emoji":"👩‍👩‍👦","aliases":["family_woman_woman_boy"]},{"emoji":"👩‍👩‍👦‍👦","aliases":["family_woman_woman_boy_boy"]},{"emoji":"👩‍👩‍👧","aliases":["family_woman_woman_girl"]},{"emoji":"👩‍👩‍👧‍👦","aliases":["family_woman_woman_girl_boy"]},{"emoji":"👩‍👩‍👧‍👧","aliases":["family_woman_woman_girl_girl"]},{"emoji":"🧑‍🌾","aliases":["farmer"]},{"emoji":"🇫🇴","aliases":["faroe_islands"]},{"emoji":"⏩","aliases":["fast_forward"]},{"emoji":"📠","aliases":["fax"]},{"emoji":"😨","aliases":["fearful"]},{"emoji":"🐾","aliases":["feet","paw_prints"]},{"emoji":"🕵️‍♀️","aliases":["female_detective"]},{"emoji":"♀️","aliases":["female_sign"]},{"emoji":"🎡","aliases":["ferris_wheel"]},{"emoji":"⛴️","aliases":["ferry"]},{"emoji":"🏑","aliases":["field_hockey"]},{"emoji":"🇫🇯","aliases":["fiji"]},{"emoji":"🗄️","aliases":["file_cabinet"]},{"emoji":"📁","aliases":["file_folder"]},{"emoji":"📽️","aliases":["film_projector"]},{"emoji":"🎞️","aliases":["film_strip"]},{"emoji":"🇫🇮","aliases":["finland"]},{"emoji":"🔥","aliases":["fire"]},{"emoji":"🚒","aliases":["fire_engine"]},{"emoji":"🧯","aliases":["fire_extinguisher"]},{"emoji":"🧨","aliases":["firecracker"]},{"emoji":"🧑‍🚒","aliases":["firefighter"]},{"emoji":"🎆","aliases":["fireworks"]},{"emoji":"🌓","aliases":["first_quarter_moon"]},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"]},{"emoji":"🐟","aliases":["fish"]},{"emoji":"🍥","aliases":["fish_cake"]},{"emoji":"🎣","aliases":["fishing_pole_and_fish"]},{"emoji":"🤛","aliases":["fist_left"]},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"]},{"emoji":"✊","aliases":["fist_raised","fist"]},{"emoji":"🤜","aliases":["fist_right"]},{"emoji":"5️⃣","aliases":["five"]},{"emoji":"🎏","aliases":["flags"]},{"emoji":"🦩","aliases":["flamingo"]},{"emoji":"🔦","aliases":["flashlight"]},{"emoji":"🥿","aliases":["flat_shoe"]},{"emoji":"⚜️","aliases":["fleur_de_lis"]},{"emoji":"🛬","aliases":["flight_arrival"]},{"emoji":"🛫","aliases":["flight_departure"]},{"emoji":"💾","aliases":["floppy_disk"]},{"emoji":"🎴","aliases":["flower_playing_cards"]},{"emoji":"😳","aliases":["flushed"]},{"emoji":"🥏","aliases":["flying_disc"]},{"emoji":"🛸","aliases":["flying_saucer"]},{"emoji":"🌫️","aliases":["fog"]},{"emoji":"🌁","aliases":["foggy"]},{"emoji":"🦶","aliases":["foot"]},{"emoji":"🏈","aliases":["football"]},{"emoji":"👣","aliases":["footprints"]},{"emoji":"🍴","aliases":["fork_and_knife"]},{"emoji":"🥠","aliases":["fortune_cookie"]},{"emoji":"⛲","aliases":["fountain"]},{"emoji":"🖋️","aliases":["fountain_pen"]},{"emoji":"4️⃣","aliases":["four"]},{"emoji":"🍀","aliases":["four_leaf_clover"]},{"emoji":"🦊","aliases":["fox_face"]},{"emoji":"🇫🇷","aliases":["fr"]},{"emoji":"🖼️","aliases":["framed_picture"]},{"emoji":"🆓","aliases":["free"]},{"emoji":"🇬🇫","aliases":["french_guiana"]},{"emoji":"🇵🇫","aliases":["french_polynesia"]},{"emoji":"🇹🇫","aliases":["french_southern_territories"]},{"emoji":"🍳","aliases":["fried_egg"]},{"emoji":"🍤","aliases":["fried_shrimp"]},{"emoji":"🍟","aliases":["fries"]},{"emoji":"🐸","aliases":["frog"]},{"emoji":"😦","aliases":["frowning"]},{"emoji":"☹️","aliases":["frowning_face"]},{"emoji":"🙍‍♂️","aliases":["frowning_man"]},{"emoji":"🙍","aliases":["frowning_person"]},{"emoji":"🙍‍♀️","aliases":["frowning_woman"]},{"emoji":"⛽","aliases":["fuelpump"]},{"emoji":"🌕","aliases":["full_moon"]},{"emoji":"🌝","aliases":["full_moon_with_face"]},{"emoji":"⚱️","aliases":["funeral_urn"]},{"emoji":"🇬🇦","aliases":["gabon"]},{"emoji":"🇬🇲","aliases":["gambia"]},{"emoji":"🎲","aliases":["game_die"]},{"emoji":"🧄","aliases":["garlic"]},{"emoji":"🇬🇧","aliases":["gb","uk"]},{"emoji":"⚙️","aliases":["gear"]},{"emoji":"💎","aliases":["gem"]},{"emoji":"♊","aliases":["gemini"]},{"emoji":"🧞","aliases":["genie"]},{"emoji":"🧞‍♂️","aliases":["genie_man"]},{"emoji":"🧞‍♀️","aliases":["genie_woman"]},{"emoji":"🇬🇪","aliases":["georgia"]},{"emoji":"🇬🇭","aliases":["ghana"]},{"emoji":"👻","aliases":["ghost"]},{"emoji":"🇬🇮","aliases":["gibraltar"]},{"emoji":"🎁","aliases":["gift"]},{"emoji":"💝","aliases":["gift_heart"]},{"emoji":"🦒","aliases":["giraffe"]},{"emoji":"👧","aliases":["girl"]},{"emoji":"🌐","aliases":["globe_with_meridians"]},{"emoji":"🧤","aliases":["gloves"]},{"emoji":"🥅","aliases":["goal_net"]},{"emoji":"🐐","aliases":["goat"]},{"emoji":"🥽","aliases":["goggles"]},{"emoji":"⛳","aliases":["golf"]},{"emoji":"🏌️","aliases":["golfing"]},{"emoji":"🏌️‍♂️","aliases":["golfing_man"]},{"emoji":"🏌️‍♀️","aliases":["golfing_woman"]},{"emoji":"🦍","aliases":["gorilla"]},{"emoji":"🍇","aliases":["grapes"]},{"emoji":"🇬🇷","aliases":["greece"]},{"emoji":"🍏","aliases":["green_apple"]},{"emoji":"📗","aliases":["green_book"]},{"emoji":"🟢","aliases":["green_circle"]},{"emoji":"💚","aliases":["green_heart"]},{"emoji":"🥗","aliases":["green_salad"]},{"emoji":"🟩","aliases":["green_square"]},{"emoji":"🇬🇱","aliases":["greenland"]},{"emoji":"🇬🇩","aliases":["grenada"]},{"emoji":"❕","aliases":["grey_exclamation"]},{"emoji":"❔","aliases":["grey_question"]},{"emoji":"😬","aliases":["grimacing"]},{"emoji":"😁","aliases":["grin"]},{"emoji":"😀","aliases":["grinning"]},{"emoji":"🇬🇵","aliases":["guadeloupe"]},{"emoji":"🇬🇺","aliases":["guam"]},{"emoji":"💂","aliases":["guard"]},{"emoji":"💂‍♂️","aliases":["guardsman"]},{"emoji":"💂‍♀️","aliases":["guardswoman"]},{"emoji":"🇬🇹","aliases":["guatemala"]},{"emoji":"🇬🇬","aliases":["guernsey"]},{"emoji":"🦮","aliases":["guide_dog"]},{"emoji":"🇬🇳","aliases":["guinea"]},{"emoji":"🇬🇼","aliases":["guinea_bissau"]},{"emoji":"🎸","aliases":["guitar"]},{"emoji":"🔫","aliases":["gun"]},{"emoji":"🇬🇾","aliases":["guyana"]},{"emoji":"💇","aliases":["haircut"]},{"emoji":"💇‍♂️","aliases":["haircut_man"]},{"emoji":"💇‍♀️","aliases":["haircut_woman"]},{"emoji":"🇭🇹","aliases":["haiti"]},{"emoji":"🍔","aliases":["hamburger"]},{"emoji":"🔨","aliases":["hammer"]},{"emoji":"⚒️","aliases":["hammer_and_pick"]},{"emoji":"🛠️","aliases":["hammer_and_wrench"]},{"emoji":"🐹","aliases":["hamster"]},{"emoji":"✋","aliases":["hand","raised_hand"]},{"emoji":"🤭","aliases":["hand_over_mouth"]},{"emoji":"👜","aliases":["handbag"]},{"emoji":"🤾","aliases":["handball_person"]},{"emoji":"🤝","aliases":["handshake"]},{"emoji":"💩","aliases":["hankey","poop","shit"]},{"emoji":"#️⃣","aliases":["hash"]},{"emoji":"🐥","aliases":["hatched_chick"]},{"emoji":"🐣","aliases":["hatching_chick"]},{"emoji":"🎧","aliases":["headphones"]},{"emoji":"🧑‍⚕️","aliases":["health_worker"]},{"emoji":"🙉","aliases":["hear_no_evil"]},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"]},{"emoji":"❤️","aliases":["heart"]},{"emoji":"💟","aliases":["heart_decoration"]},{"emoji":"😍","aliases":["heart_eyes"]},{"emoji":"😻","aliases":["heart_eyes_cat"]},{"emoji":"💓","aliases":["heartbeat"]},{"emoji":"💗","aliases":["heartpulse"]},{"emoji":"♥️","aliases":["hearts"]},{"emoji":"✔️","aliases":["heavy_check_mark"]},{"emoji":"➗","aliases":["heavy_division_sign"]},{"emoji":"💲","aliases":["heavy_dollar_sign"]},{"emoji":"❣️","aliases":["heavy_heart_exclamation"]},{"emoji":"➖","aliases":["heavy_minus_sign"]},{"emoji":"✖️","aliases":["heavy_multiplication_x"]},{"emoji":"➕","aliases":["heavy_plus_sign"]},{"emoji":"🦔","aliases":["hedgehog"]},{"emoji":"🚁","aliases":["helicopter"]},{"emoji":"🌿","aliases":["herb"]},{"emoji":"🌺","aliases":["hibiscus"]},{"emoji":"🔆","aliases":["high_brightness"]},{"emoji":"👠","aliases":["high_heel"]},{"emoji":"🥾","aliases":["hiking_boot"]},{"emoji":"🛕","aliases":["hindu_temple"]},{"emoji":"🦛","aliases":["hippopotamus"]},{"emoji":"🔪","aliases":["hocho","knife"]},{"emoji":"🕳️","aliases":["hole"]},{"emoji":"🇭🇳","aliases":["honduras"]},{"emoji":"🍯","aliases":["honey_pot"]},{"emoji":"🇭🇰","aliases":["hong_kong"]},{"emoji":"🐴","aliases":["horse"]},{"emoji":"🏇","aliases":["horse_racing"]},{"emoji":"🏥","aliases":["hospital"]},{"emoji":"🥵","aliases":["hot_face"]},{"emoji":"🌶️","aliases":["hot_pepper"]},{"emoji":"🌭","aliases":["hotdog"]},{"emoji":"🏨","aliases":["hotel"]},{"emoji":"♨️","aliases":["hotsprings"]},{"emoji":"⌛","aliases":["hourglass"]},{"emoji":"⏳","aliases":["hourglass_flowing_sand"]},{"emoji":"🏠","aliases":["house"]},{"emoji":"🏡","aliases":["house_with_garden"]},{"emoji":"🏘️","aliases":["houses"]},{"emoji":"🤗","aliases":["hugs"]},{"emoji":"🇭🇺","aliases":["hungary"]},{"emoji":"😯","aliases":["hushed"]},{"emoji":"🍨","aliases":["ice_cream"]},{"emoji":"🧊","aliases":["ice_cube"]},{"emoji":"🏒","aliases":["ice_hockey"]},{"emoji":"⛸️","aliases":["ice_skate"]},{"emoji":"🍦","aliases":["icecream"]},{"emoji":"🇮🇸","aliases":["iceland"]},{"emoji":"🆔","aliases":["id"]},{"emoji":"🉐","aliases":["ideograph_advantage"]},{"emoji":"👿","aliases":["imp"]},{"emoji":"📥","aliases":["inbox_tray"]},{"emoji":"📨","aliases":["incoming_envelope"]},{"emoji":"🇮🇳","aliases":["india"]},{"emoji":"🇮🇩","aliases":["indonesia"]},{"emoji":"♾️","aliases":["infinity"]},{"emoji":"ℹ️","aliases":["information_source"]},{"emoji":"😇","aliases":["innocent"]},{"emoji":"⁉️","aliases":["interrobang"]},{"emoji":"📱","aliases":["iphone"]},{"emoji":"🇮🇷","aliases":["iran"]},{"emoji":"🇮🇶","aliases":["iraq"]},{"emoji":"🇮🇪","aliases":["ireland"]},{"emoji":"🇮🇲","aliases":["isle_of_man"]},{"emoji":"🇮🇱","aliases":["israel"]},{"emoji":"🇮🇹","aliases":["it"]},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"]},{"emoji":"🎃","aliases":["jack_o_lantern"]},{"emoji":"🇯🇲","aliases":["jamaica"]},{"emoji":"🗾","aliases":["japan"]},{"emoji":"🏯","aliases":["japanese_castle"]},{"emoji":"👺","aliases":["japanese_goblin"]},{"emoji":"👹","aliases":["japanese_ogre"]},{"emoji":"👖","aliases":["jeans"]},{"emoji":"🇯🇪","aliases":["jersey"]},{"emoji":"🧩","aliases":["jigsaw"]},{"emoji":"🇯🇴","aliases":["jordan"]},{"emoji":"😂","aliases":["joy"]},{"emoji":"😹","aliases":["joy_cat"]},{"emoji":"🕹️","aliases":["joystick"]},{"emoji":"🇯🇵","aliases":["jp"]},{"emoji":"🧑‍⚖️","aliases":["judge"]},{"emoji":"🤹","aliases":["juggling_person"]},{"emoji":"🕋","aliases":["kaaba"]},{"emoji":"🦘","aliases":["kangaroo"]},{"emoji":"🇰🇿","aliases":["kazakhstan"]},{"emoji":"🇰🇪","aliases":["kenya"]},{"emoji":"🔑","aliases":["key"]},{"emoji":"⌨️","aliases":["keyboard"]},{"emoji":"🔟","aliases":["keycap_ten"]},{"emoji":"🛴","aliases":["kick_scooter"]},{"emoji":"👘","aliases":["kimono"]},{"emoji":"🇰🇮","aliases":["kiribati"]},{"emoji":"💋","aliases":["kiss"]},{"emoji":"😗","aliases":["kissing"]},{"emoji":"😽","aliases":["kissing_cat"]},{"emoji":"😚","aliases":["kissing_closed_eyes"]},{"emoji":"😘","aliases":["kissing_heart"]},{"emoji":"😙","aliases":["kissing_smiling_eyes"]},{"emoji":"🪁","aliases":["kite"]},{"emoji":"🥝","aliases":["kiwi_fruit"]},{"emoji":"🧎‍♂️","aliases":["kneeling_man"]},{"emoji":"🧎","aliases":["kneeling_person"]},{"emoji":"🧎‍♀️","aliases":["kneeling_woman"]},{"emoji":"🐨","aliases":["koala"]},{"emoji":"🈁","aliases":["koko"]},{"emoji":"🇽🇰","aliases":["kosovo"]},{"emoji":"🇰🇷","aliases":["kr"]},{"emoji":"🇰🇼","aliases":["kuwait"]},{"emoji":"🇰🇬","aliases":["kyrgyzstan"]},{"emoji":"🥼","aliases":["lab_coat"]},{"emoji":"🏷️","aliases":["label"]},{"emoji":"🥍","aliases":["lacrosse"]},{"emoji":"🐞","aliases":["lady_beetle"]},{"emoji":"🇱🇦","aliases":["laos"]},{"emoji":"🔵","aliases":["large_blue_circle"]},{"emoji":"🔷","aliases":["large_blue_diamond"]},{"emoji":"🔶","aliases":["large_orange_diamond"]},{"emoji":"🌗","aliases":["last_quarter_moon"]},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"]},{"emoji":"✝️","aliases":["latin_cross"]},{"emoji":"🇱🇻","aliases":["latvia"]},{"emoji":"😆","aliases":["laughing","satisfied","laugh"]},{"emoji":"🥬","aliases":["leafy_green"]},{"emoji":"🍃","aliases":["leaves"]},{"emoji":"🇱🇧","aliases":["lebanon"]},{"emoji":"📒","aliases":["ledger"]},{"emoji":"🛅","aliases":["left_luggage"]},{"emoji":"↔️","aliases":["left_right_arrow"]},{"emoji":"🗨️","aliases":["left_speech_bubble"]},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"]},{"emoji":"🦵","aliases":["leg"]},{"emoji":"🍋","aliases":["lemon"]},{"emoji":"♌","aliases":["leo"]},{"emoji":"🐆","aliases":["leopard"]},{"emoji":"🇱🇸","aliases":["lesotho"]},{"emoji":"🎚️","aliases":["level_slider"]},{"emoji":"🇱🇷","aliases":["liberia"]},{"emoji":"♎","aliases":["libra"]},{"emoji":"🇱🇾","aliases":["libya"]},{"emoji":"🇱🇮","aliases":["liechtenstein"]},{"emoji":"🚈","aliases":["light_rail"]},{"emoji":"🔗","aliases":["link"]},{"emoji":"🦁","aliases":["lion"]},{"emoji":"👄","aliases":["lips"]},{"emoji":"💄","aliases":["lipstick"]},{"emoji":"🇱🇹","aliases":["lithuania"]},{"emoji":"🦎","aliases":["lizard"]},{"emoji":"🦙","aliases":["llama"]},{"emoji":"🦞","aliases":["lobster"]},{"emoji":"🔒","aliases":["lock"]},{"emoji":"🔏","aliases":["lock_with_ink_pen"]},{"emoji":"🍭","aliases":["lollipop"]},{"emoji":"➿","aliases":["loop"]},{"emoji":"🧴","aliases":["lotion_bottle"]},{"emoji":"🧘","aliases":["lotus_position"]},{"emoji":"🧘‍♂️","aliases":["lotus_position_man"]},{"emoji":"🧘‍♀️","aliases":["lotus_position_woman"]},{"emoji":"🔊","aliases":["loud_sound"]},{"emoji":"📢","aliases":["loudspeaker"]},{"emoji":"🏩","aliases":["love_hotel"]},{"emoji":"💌","aliases":["love_letter"]},{"emoji":"🤟","aliases":["love_you_gesture"]},{"emoji":"🔅","aliases":["low_brightness"]},{"emoji":"🧳","aliases":["luggage"]},{"emoji":"🇱🇺","aliases":["luxembourg"]},{"emoji":"🤥","aliases":["lying_face"]},{"emoji":"Ⓜ️","aliases":["m"]},{"emoji":"🇲🇴","aliases":["macau"]},{"emoji":"🇲🇰","aliases":["macedonia"]},{"emoji":"🇲🇬","aliases":["madagascar"]},{"emoji":"🔍","aliases":["mag"]},{"emoji":"🔎","aliases":["mag_right"]},{"emoji":"🧙","aliases":["mage"]},{"emoji":"🧙‍♂️","aliases":["mage_man"]},{"emoji":"🧙‍♀️","aliases":["mage_woman"]},{"emoji":"🧲","aliases":["magnet"]},{"emoji":"🀄","aliases":["mahjong"]},{"emoji":"📫","aliases":["mailbox"]},{"emoji":"📪","aliases":["mailbox_closed"]},{"emoji":"📬","aliases":["mailbox_with_mail"]},{"emoji":"📭","aliases":["mailbox_with_no_mail"]},{"emoji":"🇲🇼","aliases":["malawi"]},{"emoji":"🇲🇾","aliases":["malaysia"]},{"emoji":"🇲🇻","aliases":["maldives"]},{"emoji":"🕵️‍♂️","aliases":["male_detective"]},{"emoji":"♂️","aliases":["male_sign"]},{"emoji":"🇲🇱","aliases":["mali"]},{"emoji":"🇲🇹","aliases":["malta"]},{"emoji":"👨","aliases":["man"]},{"emoji":"👨‍🎨","aliases":["man_artist"]},{"emoji":"👨‍🚀","aliases":["man_astronaut"]},{"emoji":"🤸‍♂️","aliases":["man_cartwheeling"]},{"emoji":"👨‍🍳","aliases":["man_cook"]},{"emoji":"🕺","aliases":["man_dancing"]},{"emoji":"🤦‍♂️","aliases":["man_facepalming"]},{"emoji":"👨‍🏭","aliases":["man_factory_worker"]},{"emoji":"👨‍🌾","aliases":["man_farmer"]},{"emoji":"👨‍🚒","aliases":["man_firefighter"]},{"emoji":"👨‍⚕️","aliases":["man_health_worker"]},{"emoji":"👨‍🦽","aliases":["man_in_manual_wheelchair"]},{"emoji":"👨‍🦼","aliases":["man_in_motorized_wheelchair"]},{"emoji":"👨‍⚖️","aliases":["man_judge"]},{"emoji":"🤹‍♂️","aliases":["man_juggling"]},{"emoji":"👨‍🔧","aliases":["man_mechanic"]},{"emoji":"👨‍💼","aliases":["man_office_worker"]},{"emoji":"👨‍✈️","aliases":["man_pilot"]},{"emoji":"🤾‍♂️","aliases":["man_playing_handball"]},{"emoji":"🤽‍♂️","aliases":["man_playing_water_polo"]},{"emoji":"👨‍🔬","aliases":["man_scientist"]},{"emoji":"🤷‍♂️","aliases":["man_shrugging"]},{"emoji":"👨‍🎤","aliases":["man_singer"]},{"emoji":"👨‍🎓","aliases":["man_student"]},{"emoji":"👨‍🏫","aliases":["man_teacher"]},{"emoji":"👨‍💻","aliases":["man_technologist"]},{"emoji":"👲","aliases":["man_with_gua_pi_mao"]},{"emoji":"👨‍🦯","aliases":["man_with_probing_cane"]},{"emoji":"👳‍♂️","aliases":["man_with_turban"]},{"emoji":"🥭","aliases":["mango"]},{"emoji":"👞","aliases":["mans_shoe","shoe"]},{"emoji":"🕰️","aliases":["mantelpiece_clock"]},{"emoji":"🦽","aliases":["manual_wheelchair"]},{"emoji":"🍁","aliases":["maple_leaf"]},{"emoji":"🇲🇭","aliases":["marshall_islands"]},{"emoji":"🥋","aliases":["martial_arts_uniform"]},{"emoji":"🇲🇶","aliases":["martinique"]},{"emoji":"😷","aliases":["mask"]},{"emoji":"💆","aliases":["massage"]},{"emoji":"💆‍♂️","aliases":["massage_man"]},{"emoji":"💆‍♀️","aliases":["massage_woman"]},{"emoji":"🧉","aliases":["mate"]},{"emoji":"🇲🇷","aliases":["mauritania"]},{"emoji":"🇲🇺","aliases":["mauritius"]},{"emoji":"🇾🇹","aliases":["mayotte"]},{"emoji":"🍖","aliases":["meat_on_bone"]},{"emoji":"🧑‍🔧","aliases":["mechanic"]},{"emoji":"🦾","aliases":["mechanical_arm"]},{"emoji":"🦿","aliases":["mechanical_leg"]},{"emoji":"🎖️","aliases":["medal_military"]},{"emoji":"🏅","aliases":["medal_sports"]},{"emoji":"⚕️","aliases":["medical_symbol"]},{"emoji":"📣","aliases":["mega"]},{"emoji":"🍈","aliases":["melon"]},{"emoji":"📝","aliases":["memo","pencil"]},{"emoji":"🤼‍♂️","aliases":["men_wrestling"]},{"emoji":"🕎","aliases":["menorah"]},{"emoji":"🚹","aliases":["mens"]},{"emoji":"🧜‍♀️","aliases":["mermaid"]},{"emoji":"🧜‍♂️","aliases":["merman"]},{"emoji":"🧜","aliases":["merperson"]},{"emoji":"🤘","aliases":["metal"]},{"emoji":"🚇","aliases":["metro"]},{"emoji":"🇲🇽","aliases":["mexico"]},{"emoji":"🦠","aliases":["microbe"]},{"emoji":"🇫🇲","aliases":["micronesia"]},{"emoji":"🎤","aliases":["microphone"]},{"emoji":"🔬","aliases":["microscope"]},{"emoji":"🖕","aliases":["middle_finger","fu"]},{"emoji":"🥛","aliases":["milk_glass"]},{"emoji":"🌌","aliases":["milky_way"]},{"emoji":"🚐","aliases":["minibus"]},{"emoji":"💽","aliases":["minidisc"]},{"emoji":"📴","aliases":["mobile_phone_off"]},{"emoji":"🇲🇩","aliases":["moldova"]},{"emoji":"🇲🇨","aliases":["monaco"]},{"emoji":"🤑","aliases":["money_mouth_face"]},{"emoji":"💸","aliases":["money_with_wings"]},{"emoji":"💰","aliases":["moneybag"]},{"emoji":"🇲🇳","aliases":["mongolia"]},{"emoji":"🐒","aliases":["monkey"]},{"emoji":"🐵","aliases":["monkey_face"]},{"emoji":"🧐","aliases":["monocle_face"]},{"emoji":"🚝","aliases":["monorail"]},{"emoji":"🇲🇪","aliases":["montenegro"]},{"emoji":"🇲🇸","aliases":["montserrat"]},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"]},{"emoji":"🥮","aliases":["moon_cake"]},{"emoji":"🇲🇦","aliases":["morocco"]},{"emoji":"🎓","aliases":["mortar_board"]},{"emoji":"🕌","aliases":["mosque"]},{"emoji":"🦟","aliases":["mosquito"]},{"emoji":"🛥️","aliases":["motor_boat"]},{"emoji":"🛵","aliases":["motor_scooter"]},{"emoji":"🏍️","aliases":["motorcycle"]},{"emoji":"🦼","aliases":["motorized_wheelchair"]},{"emoji":"🛣️","aliases":["motorway"]},{"emoji":"🗻","aliases":["mount_fuji"]},{"emoji":"⛰️","aliases":["mountain"]},{"emoji":"🚵","aliases":["mountain_bicyclist"]},{"emoji":"🚵‍♂️","aliases":["mountain_biking_man"]},{"emoji":"🚵‍♀️","aliases":["mountain_biking_woman"]},{"emoji":"🚠","aliases":["mountain_cableway"]},{"emoji":"🚞","aliases":["mountain_railway"]},{"emoji":"🏔️","aliases":["mountain_snow"]},{"emoji":"🐭","aliases":["mouse"]},{"emoji":"🐁","aliases":["mouse2"]},{"emoji":"🎥","aliases":["movie_camera"]},{"emoji":"🗿","aliases":["moyai"]},{"emoji":"🇲🇿","aliases":["mozambique"]},{"emoji":"🤶","aliases":["mrs_claus"]},{"emoji":"💪","aliases":["muscle"]},{"emoji":"🍄","aliases":["mushroom"]},{"emoji":"🎹","aliases":["musical_keyboard"]},{"emoji":"🎵","aliases":["musical_note"]},{"emoji":"🎼","aliases":["musical_score"]},{"emoji":"🔇","aliases":["mute"]},{"emoji":"🇲🇲","aliases":["myanmar"]},{"emoji":"💅","aliases":["nail_care"]},{"emoji":"📛","aliases":["name_badge"]},{"emoji":"🇳🇦","aliases":["namibia"]},{"emoji":"🏞️","aliases":["national_park"]},{"emoji":"🇳🇷","aliases":["nauru"]},{"emoji":"🤢","aliases":["nauseated_face"]},{"emoji":"🧿","aliases":["nazar_amulet"]},{"emoji":"👔","aliases":["necktie"]},{"emoji":"❎","aliases":["negative_squared_cross_mark"]},{"emoji":"🇳🇵","aliases":["nepal"]},{"emoji":"🤓","aliases":["nerd_face"]},{"emoji":"🇳🇱","aliases":["netherlands"]},{"emoji":"😐","aliases":["neutral_face"]},{"emoji":"🆕","aliases":["new"]},{"emoji":"🇳🇨","aliases":["new_caledonia"]},{"emoji":"🌑","aliases":["new_moon"]},{"emoji":"🌚","aliases":["new_moon_with_face"]},{"emoji":"🇳🇿","aliases":["new_zealand"]},{"emoji":"📰","aliases":["newspaper"]},{"emoji":"🗞️","aliases":["newspaper_roll"]},{"emoji":"⏭️","aliases":["next_track_button"]},{"emoji":"🆖","aliases":["ng"]},{"emoji":"🇳🇮","aliases":["nicaragua"]},{"emoji":"🇳🇪","aliases":["niger"]},{"emoji":"🇳🇬","aliases":["nigeria"]},{"emoji":"🌃","aliases":["night_with_stars"]},{"emoji":"9️⃣","aliases":["nine"]},{"emoji":"🇳🇺","aliases":["niue"]},{"emoji":"🔕","aliases":["no_bell"]},{"emoji":"🚳","aliases":["no_bicycles"]},{"emoji":"⛔","aliases":["no_entry"]},{"emoji":"🚫","aliases":["no_entry_sign"]},{"emoji":"🙅","aliases":["no_good"]},{"emoji":"🙅‍♂️","aliases":["no_good_man","ng_man"]},{"emoji":"🙅‍♀️","aliases":["no_good_woman","ng_woman"]},{"emoji":"📵","aliases":["no_mobile_phones"]},{"emoji":"😶","aliases":["no_mouth"]},{"emoji":"🚷","aliases":["no_pedestrians"]},{"emoji":"🚭","aliases":["no_smoking"]},{"emoji":"🚱","aliases":["non-potable_water"]},{"emoji":"🇳🇫","aliases":["norfolk_island"]},{"emoji":"🇰🇵","aliases":["north_korea"]},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"]},{"emoji":"🇳🇴","aliases":["norway"]},{"emoji":"👃","aliases":["nose"]},{"emoji":"📓","aliases":["notebook"]},{"emoji":"📔","aliases":["notebook_with_decorative_cover"]},{"emoji":"🎶","aliases":["notes"]},{"emoji":"🔩","aliases":["nut_and_bolt"]},{"emoji":"⭕","aliases":["o"]},{"emoji":"🅾️","aliases":["o2"]},{"emoji":"🌊","aliases":["ocean"]},{"emoji":"🐙","aliases":["octopus"]},{"emoji":"🍢","aliases":["oden"]},{"emoji":"🏢","aliases":["office"]},{"emoji":"🧑‍💼","aliases":["office_worker"]},{"emoji":"🛢️","aliases":["oil_drum"]},{"emoji":"🆗","aliases":["ok"]},{"emoji":"👌","aliases":["ok_hand"]},{"emoji":"🙆‍♂️","aliases":["ok_man"]},{"emoji":"🙆","aliases":["ok_person"]},{"emoji":"🙆‍♀️","aliases":["ok_woman"]},{"emoji":"🗝️","aliases":["old_key"]},{"emoji":"🧓","aliases":["older_adult"]},{"emoji":"👴","aliases":["older_man"]},{"emoji":"👵","aliases":["older_woman"]},{"emoji":"🕉️","aliases":["om"]},{"emoji":"🇴🇲","aliases":["oman"]},{"emoji":"🔛","aliases":["on"]},{"emoji":"🚘","aliases":["oncoming_automobile"]},{"emoji":"🚍","aliases":["oncoming_bus"]},{"emoji":"🚔","aliases":["oncoming_police_car"]},{"emoji":"🚖","aliases":["oncoming_taxi"]},{"emoji":"1️⃣","aliases":["one"]},{"emoji":"🩱","aliases":["one_piece_swimsuit"]},{"emoji":"🧅","aliases":["onion"]},{"emoji":"📂","aliases":["open_file_folder"]},{"emoji":"👐","aliases":["open_hands"]},{"emoji":"😮","aliases":["open_mouth"]},{"emoji":"☂️","aliases":["open_umbrella"]},{"emoji":"⛎","aliases":["ophiuchus"]},{"emoji":"📙","aliases":["orange_book"]},{"emoji":"🟠","aliases":["orange_circle"]},{"emoji":"🧡","aliases":["orange_heart"]},{"emoji":"🟧","aliases":["orange_square"]},{"emoji":"🦧","aliases":["orangutan"]},{"emoji":"☦️","aliases":["orthodox_cross"]},{"emoji":"🦦","aliases":["otter"]},{"emoji":"📤","aliases":["outbox_tray"]},{"emoji":"🦉","aliases":["owl"]},{"emoji":"🐂","aliases":["ox"]},{"emoji":"🦪","aliases":["oyster"]},{"emoji":"📦","aliases":["package"]},{"emoji":"📄","aliases":["page_facing_up"]},{"emoji":"📃","aliases":["page_with_curl"]},{"emoji":"📟","aliases":["pager"]},{"emoji":"🖌️","aliases":["paintbrush"]},{"emoji":"🇵🇰","aliases":["pakistan"]},{"emoji":"🇵🇼","aliases":["palau"]},{"emoji":"🇵🇸","aliases":["palestinian_territories"]},{"emoji":"🌴","aliases":["palm_tree"]},{"emoji":"🤲","aliases":["palms_up_together"]},{"emoji":"🇵🇦","aliases":["panama"]},{"emoji":"🥞","aliases":["pancakes"]},{"emoji":"🐼","aliases":["panda_face"]},{"emoji":"📎","aliases":["paperclip"]},{"emoji":"🖇️","aliases":["paperclips"]},{"emoji":"🇵🇬","aliases":["papua_new_guinea"]},{"emoji":"🪂","aliases":["parachute"]},{"emoji":"🇵🇾","aliases":["paraguay"]},{"emoji":"⛱️","aliases":["parasol_on_ground"]},{"emoji":"🅿️","aliases":["parking"]},{"emoji":"🦜","aliases":["parrot"]},{"emoji":"〽️","aliases":["part_alternation_mark"]},{"emoji":"⛅","aliases":["partly_sunny"]},{"emoji":"🥳","aliases":["partying_face"]},{"emoji":"🛳️","aliases":["passenger_ship"]},{"emoji":"🛂","aliases":["passport_control"]},{"emoji":"⏸️","aliases":["pause_button"]},{"emoji":"☮️","aliases":["peace_symbol"]},{"emoji":"🍑","aliases":["peach"]},{"emoji":"🦚","aliases":["peacock"]},{"emoji":"🥜","aliases":["peanuts"]},{"emoji":"🍐","aliases":["pear"]},{"emoji":"🖊️","aliases":["pen"]},{"emoji":"✏️","aliases":["pencil2"]},{"emoji":"🐧","aliases":["penguin"]},{"emoji":"😔","aliases":["pensive"]},{"emoji":"🧑‍🤝‍🧑","aliases":["people_holding_hands"]},{"emoji":"🎭","aliases":["performing_arts"]},{"emoji":"😣","aliases":["persevere"]},{"emoji":"🧑‍🦲","aliases":["person_bald"]},{"emoji":"🧑‍🦱","aliases":["person_curly_hair"]},{"emoji":"🤺","aliases":["person_fencing"]},{"emoji":"🧑‍🦽","aliases":["person_in_manual_wheelchair"]},{"emoji":"🧑‍🦼","aliases":["person_in_motorized_wheelchair"]},{"emoji":"🤵","aliases":["person_in_tuxedo"]},{"emoji":"🧑‍🦰","aliases":["person_red_hair"]},{"emoji":"🧑‍🦳","aliases":["person_white_hair"]},{"emoji":"🧑‍🦯","aliases":["person_with_probing_cane"]},{"emoji":"👳","aliases":["person_with_turban"]},{"emoji":"👰","aliases":["person_with_veil"]},{"emoji":"🇵🇪","aliases":["peru"]},{"emoji":"🧫","aliases":["petri_dish"]},{"emoji":"🇵🇭","aliases":["philippines"]},{"emoji":"☎️","aliases":["phone","telephone"]},{"emoji":"⛏️","aliases":["pick"]},{"emoji":"🥧","aliases":["pie"]},{"emoji":"🐷","aliases":["pig"]},{"emoji":"🐖","aliases":["pig2"]},{"emoji":"🐽","aliases":["pig_nose"]},{"emoji":"💊","aliases":["pill"]},{"emoji":"🧑‍✈️","aliases":["pilot"]},{"emoji":"🤏","aliases":["pinching_hand"]},{"emoji":"🍍","aliases":["pineapple"]},{"emoji":"🏓","aliases":["ping_pong"]},{"emoji":"🏴‍☠️","aliases":["pirate_flag"]},{"emoji":"♓","aliases":["pisces"]},{"emoji":"🇵🇳","aliases":["pitcairn_islands"]},{"emoji":"🍕","aliases":["pizza"]},{"emoji":"🛐","aliases":["place_of_worship"]},{"emoji":"🍽️","aliases":["plate_with_cutlery"]},{"emoji":"⏯️","aliases":["play_or_pause_button"]},{"emoji":"🥺","aliases":["pleading_face"]},{"emoji":"👇","aliases":["point_down"]},{"emoji":"👈","aliases":["point_left"]},{"emoji":"👉","aliases":["point_right"]},{"emoji":"☝️","aliases":["point_up"]},{"emoji":"👆","aliases":["point_up_2"]},{"emoji":"🇵🇱","aliases":["poland"]},{"emoji":"🚓","aliases":["police_car"]},{"emoji":"👮","aliases":["police_officer","cop"]},{"emoji":"👮‍♂️","aliases":["policeman"]},{"emoji":"👮‍♀️","aliases":["policewoman"]},{"emoji":"🐩","aliases":["poodle"]},{"emoji":"🍿","aliases":["popcorn"]},{"emoji":"🇵🇹","aliases":["portugal"]},{"emoji":"🏣","aliases":["post_office"]},{"emoji":"📯","aliases":["postal_horn"]},{"emoji":"📮","aliases":["postbox"]},{"emoji":"🚰","aliases":["potable_water"]},{"emoji":"🥔","aliases":["potato"]},{"emoji":"👝","aliases":["pouch"]},{"emoji":"🍗","aliases":["poultry_leg"]},{"emoji":"💷","aliases":["pound"]},{"emoji":"😾","aliases":["pouting_cat"]},{"emoji":"🙎","aliases":["pouting_face"]},{"emoji":"🙎‍♂️","aliases":["pouting_man"]},{"emoji":"🙎‍♀️","aliases":["pouting_woman"]},{"emoji":"🙏","aliases":["pray"]},{"emoji":"📿","aliases":["prayer_beads"]},{"emoji":"🤰","aliases":["pregnant_woman"]},{"emoji":"🥨","aliases":["pretzel"]},{"emoji":"⏮️","aliases":["previous_track_button"]},{"emoji":"🤴","aliases":["prince"]},{"emoji":"👸","aliases":["princess"]},{"emoji":"🖨️","aliases":["printer"]},{"emoji":"🦯","aliases":["probing_cane"]},{"emoji":"🇵🇷","aliases":["puerto_rico"]},{"emoji":"🟣","aliases":["purple_circle"]},{"emoji":"💜","aliases":["purple_heart"]},{"emoji":"🟪","aliases":["purple_square"]},{"emoji":"👛","aliases":["purse"]},{"emoji":"📌","aliases":["pushpin"]},{"emoji":"🚮","aliases":["put_litter_in_its_place"]},{"emoji":"🇶🇦","aliases":["qatar"]},{"emoji":"❓","aliases":["question"]},{"emoji":"🐰","aliases":["rabbit"]},{"emoji":"🐇","aliases":["rabbit2"]},{"emoji":"🦝","aliases":["raccoon"]},{"emoji":"🐎","aliases":["racehorse"]},{"emoji":"🏎️","aliases":["racing_car"]},{"emoji":"📻","aliases":["radio"]},{"emoji":"🔘","aliases":["radio_button"]},{"emoji":"☢️","aliases":["radioactive"]},{"emoji":"😡","aliases":["rage","pout"]},{"emoji":"🚃","aliases":["railway_car"]},{"emoji":"🛤️","aliases":["railway_track"]},{"emoji":"🌈","aliases":["rainbow"]},{"emoji":"🏳️‍🌈","aliases":["rainbow_flag"]},{"emoji":"🤚","aliases":["raised_back_of_hand"]},{"emoji":"🤨","aliases":["raised_eyebrow"]},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"]},{"emoji":"🙌","aliases":["raised_hands"]},{"emoji":"🙋","aliases":["raising_hand"]},{"emoji":"🙋‍♂️","aliases":["raising_hand_man"]},{"emoji":"🙋‍♀️","aliases":["raising_hand_woman"]},{"emoji":"🐏","aliases":["ram"]},{"emoji":"🍜","aliases":["ramen"]},{"emoji":"🐀","aliases":["rat"]},{"emoji":"🪒","aliases":["razor"]},{"emoji":"🧾","aliases":["receipt"]},{"emoji":"⏺️","aliases":["record_button"]},{"emoji":"♻️","aliases":["recycle"]},{"emoji":"🔴","aliases":["red_circle"]},{"emoji":"🧧","aliases":["red_envelope"]},{"emoji":"👨‍🦰","aliases":["red_haired_man"]},{"emoji":"👩‍🦰","aliases":["red_haired_woman"]},{"emoji":"🟥","aliases":["red_square"]},{"emoji":"®️","aliases":["registered"]},{"emoji":"☺️","aliases":["relaxed"]},{"emoji":"😌","aliases":["relieved"]},{"emoji":"🎗️","aliases":["reminder_ribbon"]},{"emoji":"🔁","aliases":["repeat"]},{"emoji":"🔂","aliases":["repeat_one"]},{"emoji":"⛑️","aliases":["rescue_worker_helmet"]},{"emoji":"🚻","aliases":["restroom"]},{"emoji":"🇷🇪","aliases":["reunion"]},{"emoji":"💞","aliases":["revolving_hearts"]},{"emoji":"⏪","aliases":["rewind"]},{"emoji":"🦏","aliases":["rhinoceros"]},{"emoji":"🎀","aliases":["ribbon"]},{"emoji":"🍚","aliases":["rice"]},{"emoji":"🍙","aliases":["rice_ball"]},{"emoji":"🍘","aliases":["rice_cracker"]},{"emoji":"🎑","aliases":["rice_scene"]},{"emoji":"🗯️","aliases":["right_anger_bubble"]},{"emoji":"💍","aliases":["ring"]},{"emoji":"🪐","aliases":["ringed_planet"]},{"emoji":"🤖","aliases":["robot"]},{"emoji":"🚀","aliases":["rocket"]},{"emoji":"🤣","aliases":["rofl"]},{"emoji":"🙄","aliases":["roll_eyes"]},{"emoji":"🧻","aliases":["roll_of_paper"]},{"emoji":"🎢","aliases":["roller_coaster"]},{"emoji":"🇷🇴","aliases":["romania"]},{"emoji":"🐓","aliases":["rooster"]},{"emoji":"🌹","aliases":["rose"]},{"emoji":"🏵️","aliases":["rosette"]},{"emoji":"🚨","aliases":["rotating_light"]},{"emoji":"📍","aliases":["round_pushpin"]},{"emoji":"🚣","aliases":["rowboat"]},{"emoji":"🚣‍♂️","aliases":["rowing_man"]},{"emoji":"🚣‍♀️","aliases":["rowing_woman"]},{"emoji":"🇷🇺","aliases":["ru"]},{"emoji":"🏉","aliases":["rugby_football"]},{"emoji":"🏃","aliases":["runner","running"]},{"emoji":"🏃‍♂️","aliases":["running_man"]},{"emoji":"🎽","aliases":["running_shirt_with_sash"]},{"emoji":"🏃‍♀️","aliases":["running_woman"]},{"emoji":"🇷🇼","aliases":["rwanda"]},{"emoji":"🈂️","aliases":["sa"]},{"emoji":"🧷","aliases":["safety_pin"]},{"emoji":"🦺","aliases":["safety_vest"]},{"emoji":"♐","aliases":["sagittarius"]},{"emoji":"🍶","aliases":["sake"]},{"emoji":"🧂","aliases":["salt"]},{"emoji":"🇼🇸","aliases":["samoa"]},{"emoji":"🇸🇲","aliases":["san_marino"]},{"emoji":"👡","aliases":["sandal"]},{"emoji":"🥪","aliases":["sandwich"]},{"emoji":"🎅","aliases":["santa"]},{"emoji":"🇸🇹","aliases":["sao_tome_principe"]},{"emoji":"🥻","aliases":["sari"]},{"emoji":"📡","aliases":["satellite"]},{"emoji":"🇸🇦","aliases":["saudi_arabia"]},{"emoji":"🧖‍♂️","aliases":["sauna_man"]},{"emoji":"🧖","aliases":["sauna_person"]},{"emoji":"🧖‍♀️","aliases":["sauna_woman"]},{"emoji":"🦕","aliases":["sauropod"]},{"emoji":"🎷","aliases":["saxophone"]},{"emoji":"🧣","aliases":["scarf"]},{"emoji":"🏫","aliases":["school"]},{"emoji":"🎒","aliases":["school_satchel"]},{"emoji":"🧑‍🔬","aliases":["scientist"]},{"emoji":"✂️","aliases":["scissors"]},{"emoji":"🦂","aliases":["scorpion"]},{"emoji":"♏","aliases":["scorpius"]},{"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","aliases":["scotland"]},{"emoji":"😱","aliases":["scream"]},{"emoji":"🙀","aliases":["scream_cat"]},{"emoji":"📜","aliases":["scroll"]},{"emoji":"💺","aliases":["seat"]},{"emoji":"㊙️","aliases":["secret"]},{"emoji":"🙈","aliases":["see_no_evil"]},{"emoji":"🌱","aliases":["seedling"]},{"emoji":"🤳","aliases":["selfie"]},{"emoji":"🇸🇳","aliases":["senegal"]},{"emoji":"🇷🇸","aliases":["serbia"]},{"emoji":"🐕‍🦺","aliases":["service_dog"]},{"emoji":"7️⃣","aliases":["seven"]},{"emoji":"🇸🇨","aliases":["seychelles"]},{"emoji":"🥘","aliases":["shallow_pan_of_food"]},{"emoji":"☘️","aliases":["shamrock"]},{"emoji":"🦈","aliases":["shark"]},{"emoji":"🍧","aliases":["shaved_ice"]},{"emoji":"🐑","aliases":["sheep"]},{"emoji":"🐚","aliases":["shell"]},{"emoji":"🛡️","aliases":["shield"]},{"emoji":"⛩️","aliases":["shinto_shrine"]},{"emoji":"🚢","aliases":["ship"]},{"emoji":"👕","aliases":["shirt","tshirt"]},{"emoji":"🛍️","aliases":["shopping"]},{"emoji":"🛒","aliases":["shopping_cart"]},{"emoji":"🩳","aliases":["shorts"]},{"emoji":"🚿","aliases":["shower"]},{"emoji":"🦐","aliases":["shrimp"]},{"emoji":"🤷","aliases":["shrug"]},{"emoji":"🤫","aliases":["shushing_face"]},{"emoji":"🇸🇱","aliases":["sierra_leone"]},{"emoji":"📶","aliases":["signal_strength"]},{"emoji":"🇸🇬","aliases":["singapore"]},{"emoji":"🧑‍🎤","aliases":["singer"]},{"emoji":"🇸🇽","aliases":["sint_maarten"]},{"emoji":"6️⃣","aliases":["six"]},{"emoji":"🔯","aliases":["six_pointed_star"]},{"emoji":"🛹","aliases":["skateboard"]},{"emoji":"🎿","aliases":["ski"]},{"emoji":"⛷️","aliases":["skier"]},{"emoji":"💀","aliases":["skull"]},{"emoji":"☠️","aliases":["skull_and_crossbones"]},{"emoji":"🦨","aliases":["skunk"]},{"emoji":"🛷","aliases":["sled"]},{"emoji":"😴","aliases":["sleeping"]},{"emoji":"🛌","aliases":["sleeping_bed"]},{"emoji":"😪","aliases":["sleepy"]},{"emoji":"🙁","aliases":["slightly_frowning_face"]},{"emoji":"🙂","aliases":["slightly_smiling_face"]},{"emoji":"🎰","aliases":["slot_machine"]},{"emoji":"🦥","aliases":["sloth"]},{"emoji":"🇸🇰","aliases":["slovakia"]},{"emoji":"🇸🇮","aliases":["slovenia"]},{"emoji":"🛩️","aliases":["small_airplane"]},{"emoji":"🔹","aliases":["small_blue_diamond"]},{"emoji":"🔸","aliases":["small_orange_diamond"]},{"emoji":"🔺","aliases":["small_red_triangle"]},{"emoji":"🔻","aliases":["small_red_triangle_down"]},{"emoji":"😄","aliases":["smile"]},{"emoji":"😸","aliases":["smile_cat"]},{"emoji":"😃","aliases":["smiley"]},{"emoji":"😺","aliases":["smiley_cat"]},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"]},{"emoji":"😈","aliases":["smiling_imp"]},{"emoji":"😏","aliases":["smirk"]},{"emoji":"😼","aliases":["smirk_cat"]},{"emoji":"🚬","aliases":["smoking"]},{"emoji":"🐌","aliases":["snail"]},{"emoji":"🐍","aliases":["snake"]},{"emoji":"🤧","aliases":["sneezing_face"]},{"emoji":"🏂","aliases":["snowboarder"]},{"emoji":"❄️","aliases":["snowflake"]},{"emoji":"⛄","aliases":["snowman"]},{"emoji":"☃️","aliases":["snowman_with_snow"]},{"emoji":"🧼","aliases":["soap"]},{"emoji":"😭","aliases":["sob"]},{"emoji":"⚽","aliases":["soccer"]},{"emoji":"🧦","aliases":["socks"]},{"emoji":"🥎","aliases":["softball"]},{"emoji":"🇸🇧","aliases":["solomon_islands"]},{"emoji":"🇸🇴","aliases":["somalia"]},{"emoji":"🔜","aliases":["soon"]},{"emoji":"🆘","aliases":["sos"]},{"emoji":"🔉","aliases":["sound"]},{"emoji":"🇿🇦","aliases":["south_africa"]},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"]},{"emoji":"🇸🇸","aliases":["south_sudan"]},{"emoji":"👾","aliases":["space_invader"]},{"emoji":"♠️","aliases":["spades"]},{"emoji":"🍝","aliases":["spaghetti"]},{"emoji":"❇️","aliases":["sparkle"]},{"emoji":"🎇","aliases":["sparkler"]},{"emoji":"✨","aliases":["sparkles"]},{"emoji":"💖","aliases":["sparkling_heart"]},{"emoji":"🙊","aliases":["speak_no_evil"]},{"emoji":"🔈","aliases":["speaker"]},{"emoji":"🗣️","aliases":["speaking_head"]},{"emoji":"💬","aliases":["speech_balloon"]},{"emoji":"🚤","aliases":["speedboat"]},{"emoji":"🕷️","aliases":["spider"]},{"emoji":"🕸️","aliases":["spider_web"]},{"emoji":"🗓️","aliases":["spiral_calendar"]},{"emoji":"🗒️","aliases":["spiral_notepad"]},{"emoji":"🧽","aliases":["sponge"]},{"emoji":"🥄","aliases":["spoon"]},{"emoji":"🦑","aliases":["squid"]},{"emoji":"🇱🇰","aliases":["sri_lanka"]},{"emoji":"🇧🇱","aliases":["st_barthelemy"]},{"emoji":"🇸🇭","aliases":["st_helena"]},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"]},{"emoji":"🇱🇨","aliases":["st_lucia"]},{"emoji":"🇲🇫","aliases":["st_martin"]},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"]},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"]},{"emoji":"🏟️","aliases":["stadium"]},{"emoji":"🧍‍♂️","aliases":["standing_man"]},{"emoji":"🧍","aliases":["standing_person"]},{"emoji":"🧍‍♀️","aliases":["standing_woman"]},{"emoji":"⭐","aliases":["star"]},{"emoji":"🌟","aliases":["star2"]},{"emoji":"☪️","aliases":["star_and_crescent"]},{"emoji":"✡️","aliases":["star_of_david"]},{"emoji":"🤩","aliases":["star_struck"]},{"emoji":"🌠","aliases":["stars"]},{"emoji":"🚉","aliases":["station"]},{"emoji":"🗽","aliases":["statue_of_liberty"]},{"emoji":"🚂","aliases":["steam_locomotive"]},{"emoji":"🩺","aliases":["stethoscope"]},{"emoji":"🍲","aliases":["stew"]},{"emoji":"⏹️","aliases":["stop_button"]},{"emoji":"🛑","aliases":["stop_sign"]},{"emoji":"⏱️","aliases":["stopwatch"]},{"emoji":"📏","aliases":["straight_ruler"]},{"emoji":"🍓","aliases":["strawberry"]},{"emoji":"😛","aliases":["stuck_out_tongue"]},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"]},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"]},{"emoji":"🧑‍🎓","aliases":["student"]},{"emoji":"🎙️","aliases":["studio_microphone"]},{"emoji":"🥙","aliases":["stuffed_flatbread"]},{"emoji":"🇸🇩","aliases":["sudan"]},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"]},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"]},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"]},{"emoji":"🌞","aliases":["sun_with_face"]},{"emoji":"🌻","aliases":["sunflower"]},{"emoji":"😎","aliases":["sunglasses"]},{"emoji":"☀️","aliases":["sunny"]},{"emoji":"🌅","aliases":["sunrise"]},{"emoji":"🌄","aliases":["sunrise_over_mountains"]},{"emoji":"🦸","aliases":["superhero"]},{"emoji":"🦸‍♂️","aliases":["superhero_man"]},{"emoji":"🦸‍♀️","aliases":["superhero_woman"]},{"emoji":"🦹","aliases":["supervillain"]},{"emoji":"🦹‍♂️","aliases":["supervillain_man"]},{"emoji":"🦹‍♀️","aliases":["supervillain_woman"]},{"emoji":"🏄","aliases":["surfer"]},{"emoji":"🏄‍♂️","aliases":["surfing_man"]},{"emoji":"🏄‍♀️","aliases":["surfing_woman"]},{"emoji":"🇸🇷","aliases":["suriname"]},{"emoji":"🍣","aliases":["sushi"]},{"emoji":"🚟","aliases":["suspension_railway"]},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"]},{"emoji":"🦢","aliases":["swan"]},{"emoji":"🇸🇿","aliases":["swaziland"]},{"emoji":"😓","aliases":["sweat"]},{"emoji":"💦","aliases":["sweat_drops"]},{"emoji":"😅","aliases":["sweat_smile"]},{"emoji":"🇸🇪","aliases":["sweden"]},{"emoji":"🍠","aliases":["sweet_potato"]},{"emoji":"🩲","aliases":["swim_brief"]},{"emoji":"🏊","aliases":["swimmer"]},{"emoji":"🏊‍♂️","aliases":["swimming_man"]},{"emoji":"🏊‍♀️","aliases":["swimming_woman"]},{"emoji":"🇨🇭","aliases":["switzerland"]},{"emoji":"🔣","aliases":["symbols"]},{"emoji":"🕍","aliases":["synagogue"]},{"emoji":"🇸🇾","aliases":["syria"]},{"emoji":"💉","aliases":["syringe"]},{"emoji":"🦖","aliases":["t-rex"]},{"emoji":"🌮","aliases":["taco"]},{"emoji":"🎉","aliases":["tada","hooray"]},{"emoji":"🇹🇼","aliases":["taiwan"]},{"emoji":"🇹🇯","aliases":["tajikistan"]},{"emoji":"🥡","aliases":["takeout_box"]},{"emoji":"🎋","aliases":["tanabata_tree"]},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"]},{"emoji":"🇹🇿","aliases":["tanzania"]},{"emoji":"♉","aliases":["taurus"]},{"emoji":"🚕","aliases":["taxi"]},{"emoji":"🍵","aliases":["tea"]},{"emoji":"🧑‍🏫","aliases":["teacher"]},{"emoji":"🧑‍💻","aliases":["technologist"]},{"emoji":"🧸","aliases":["teddy_bear"]},{"emoji":"📞","aliases":["telephone_receiver"]},{"emoji":"🔭","aliases":["telescope"]},{"emoji":"🎾","aliases":["tennis"]},{"emoji":"⛺","aliases":["tent"]},{"emoji":"🧪","aliases":["test_tube"]},{"emoji":"🇹🇭","aliases":["thailand"]},{"emoji":"🌡️","aliases":["thermometer"]},{"emoji":"🤔","aliases":["thinking"]},{"emoji":"💭","aliases":["thought_balloon"]},{"emoji":"🧵","aliases":["thread"]},{"emoji":"3️⃣","aliases":["three"]},{"emoji":"🎫","aliases":["ticket"]},{"emoji":"🎟️","aliases":["tickets"]},{"emoji":"🐯","aliases":["tiger"]},{"emoji":"🐅","aliases":["tiger2"]},{"emoji":"⏲️","aliases":["timer_clock"]},{"emoji":"🇹🇱","aliases":["timor_leste"]},{"emoji":"💁‍♂️","aliases":["tipping_hand_man","sassy_man"]},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"]},{"emoji":"💁‍♀️","aliases":["tipping_hand_woman","sassy_woman"]},{"emoji":"😫","aliases":["tired_face"]},{"emoji":"™️","aliases":["tm"]},{"emoji":"🇹🇬","aliases":["togo"]},{"emoji":"🚽","aliases":["toilet"]},{"emoji":"🇹🇰","aliases":["tokelau"]},{"emoji":"🗼","aliases":["tokyo_tower"]},{"emoji":"🍅","aliases":["tomato"]},{"emoji":"🇹🇴","aliases":["tonga"]},{"emoji":"👅","aliases":["tongue"]},{"emoji":"🧰","aliases":["toolbox"]},{"emoji":"🦷","aliases":["tooth"]},{"emoji":"🔝","aliases":["top"]},{"emoji":"🎩","aliases":["tophat"]},{"emoji":"🌪️","aliases":["tornado"]},{"emoji":"🇹🇷","aliases":["tr"]},{"emoji":"🖲️","aliases":["trackball"]},{"emoji":"🚜","aliases":["tractor"]},{"emoji":"🚥","aliases":["traffic_light"]},{"emoji":"🚋","aliases":["train"]},{"emoji":"🚆","aliases":["train2"]},{"emoji":"🚊","aliases":["tram"]},{"emoji":"🚩","aliases":["triangular_flag_on_post"]},{"emoji":"📐","aliases":["triangular_ruler"]},{"emoji":"🔱","aliases":["trident"]},{"emoji":"🇹🇹","aliases":["trinidad_tobago"]},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"]},{"emoji":"😤","aliases":["triumph"]},{"emoji":"🚎","aliases":["trolleybus"]},{"emoji":"🏆","aliases":["trophy"]},{"emoji":"🍹","aliases":["tropical_drink"]},{"emoji":"🐠","aliases":["tropical_fish"]},{"emoji":"🚚","aliases":["truck"]},{"emoji":"🎺","aliases":["trumpet"]},{"emoji":"🌷","aliases":["tulip"]},{"emoji":"🥃","aliases":["tumbler_glass"]},{"emoji":"🇹🇳","aliases":["tunisia"]},{"emoji":"🦃","aliases":["turkey"]},{"emoji":"🇹🇲","aliases":["turkmenistan"]},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"]},{"emoji":"🐢","aliases":["turtle"]},{"emoji":"🇹🇻","aliases":["tuvalu"]},{"emoji":"📺","aliases":["tv"]},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"]},{"emoji":"2️⃣","aliases":["two"]},{"emoji":"💕","aliases":["two_hearts"]},{"emoji":"👬","aliases":["two_men_holding_hands"]},{"emoji":"👭","aliases":["two_women_holding_hands"]},{"emoji":"🈹","aliases":["u5272"]},{"emoji":"🈴","aliases":["u5408"]},{"emoji":"🈺","aliases":["u55b6"]},{"emoji":"🈯","aliases":["u6307"]},{"emoji":"🈷️","aliases":["u6708"]},{"emoji":"🈶","aliases":["u6709"]},{"emoji":"🈵","aliases":["u6e80"]},{"emoji":"🈚","aliases":["u7121"]},{"emoji":"🈸","aliases":["u7533"]},{"emoji":"🈲","aliases":["u7981"]},{"emoji":"🈳","aliases":["u7a7a"]},{"emoji":"🇺🇬","aliases":["uganda"]},{"emoji":"🇺🇦","aliases":["ukraine"]},{"emoji":"☔","aliases":["umbrella"]},{"emoji":"😒","aliases":["unamused"]},{"emoji":"🔞","aliases":["underage"]},{"emoji":"🦄","aliases":["unicorn"]},{"emoji":"🇦🇪","aliases":["united_arab_emirates"]},{"emoji":"🇺🇳","aliases":["united_nations"]},{"emoji":"🔓","aliases":["unlock"]},{"emoji":"🆙","aliases":["up"]},{"emoji":"🙃","aliases":["upside_down_face"]},{"emoji":"🇺🇾","aliases":["uruguay"]},{"emoji":"🇺🇸","aliases":["us"]},{"emoji":"🇺🇲","aliases":["us_outlying_islands"]},{"emoji":"🇻🇮","aliases":["us_virgin_islands"]},{"emoji":"🇺🇿","aliases":["uzbekistan"]},{"emoji":"✌️","aliases":["v"]},{"emoji":"🧛","aliases":["vampire"]},{"emoji":"🧛‍♂️","aliases":["vampire_man"]},{"emoji":"🧛‍♀️","aliases":["vampire_woman"]},{"emoji":"🇻🇺","aliases":["vanuatu"]},{"emoji":"🇻🇦","aliases":["vatican_city"]},{"emoji":"🇻🇪","aliases":["venezuela"]},{"emoji":"🚦","aliases":["vertical_traffic_light"]},{"emoji":"📼","aliases":["vhs"]},{"emoji":"📳","aliases":["vibration_mode"]},{"emoji":"📹","aliases":["video_camera"]},{"emoji":"🎮","aliases":["video_game"]},{"emoji":"🇻🇳","aliases":["vietnam"]},{"emoji":"🎻","aliases":["violin"]},{"emoji":"♍","aliases":["virgo"]},{"emoji":"🌋","aliases":["volcano"]},{"emoji":"🏐","aliases":["volleyball"]},{"emoji":"🤮","aliases":["vomiting_face"]},{"emoji":"🆚","aliases":["vs"]},{"emoji":"🖖","aliases":["vulcan_salute"]},{"emoji":"🧇","aliases":["waffle"]},{"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","aliases":["wales"]},{"emoji":"🚶","aliases":["walking"]},{"emoji":"🚶‍♂️","aliases":["walking_man"]},{"emoji":"🚶‍♀️","aliases":["walking_woman"]},{"emoji":"🇼🇫","aliases":["wallis_futuna"]},{"emoji":"🌘","aliases":["waning_crescent_moon"]},{"emoji":"🌖","aliases":["waning_gibbous_moon"]},{"emoji":"⚠️","aliases":["warning"]},{"emoji":"🗑️","aliases":["wastebasket"]},{"emoji":"⌚","aliases":["watch"]},{"emoji":"🐃","aliases":["water_buffalo"]},{"emoji":"🤽","aliases":["water_polo"]},{"emoji":"🍉","aliases":["watermelon"]},{"emoji":"👋","aliases":["wave"]},{"emoji":"〰️","aliases":["wavy_dash"]},{"emoji":"🌒","aliases":["waxing_crescent_moon"]},{"emoji":"🚾","aliases":["wc"]},{"emoji":"😩","aliases":["weary"]},{"emoji":"💒","aliases":["wedding"]},{"emoji":"🏋️","aliases":["weight_lifting"]},{"emoji":"🏋️‍♂️","aliases":["weight_lifting_man"]},{"emoji":"🏋️‍♀️","aliases":["weight_lifting_woman"]},{"emoji":"🇪🇭","aliases":["western_sahara"]},{"emoji":"🐳","aliases":["whale"]},{"emoji":"🐋","aliases":["whale2"]},{"emoji":"☸️","aliases":["wheel_of_dharma"]},{"emoji":"♿","aliases":["wheelchair"]},{"emoji":"✅","aliases":["white_check_mark"]},{"emoji":"⚪","aliases":["white_circle"]},{"emoji":"🏳️","aliases":["white_flag"]},{"emoji":"💮","aliases":["white_flower"]},{"emoji":"👨‍🦳","aliases":["white_haired_man"]},{"emoji":"👩‍🦳","aliases":["white_haired_woman"]},{"emoji":"🤍","aliases":["white_heart"]},{"emoji":"⬜","aliases":["white_large_square"]},{"emoji":"◽","aliases":["white_medium_small_square"]},{"emoji":"◻️","aliases":["white_medium_square"]},{"emoji":"▫️","aliases":["white_small_square"]},{"emoji":"🔳","aliases":["white_square_button"]},{"emoji":"🥀","aliases":["wilted_flower"]},{"emoji":"🎐","aliases":["wind_chime"]},{"emoji":"🌬️","aliases":["wind_face"]},{"emoji":"🍷","aliases":["wine_glass"]},{"emoji":"😉","aliases":["wink"]},{"emoji":"🐺","aliases":["wolf"]},{"emoji":"👩","aliases":["woman"]},{"emoji":"👩‍🎨","aliases":["woman_artist"]},{"emoji":"👩‍🚀","aliases":["woman_astronaut"]},{"emoji":"🤸‍♀️","aliases":["woman_cartwheeling"]},{"emoji":"👩‍🍳","aliases":["woman_cook"]},{"emoji":"💃","aliases":["woman_dancing","dancer"]},{"emoji":"🤦‍♀️","aliases":["woman_facepalming"]},{"emoji":"👩‍🏭","aliases":["woman_factory_worker"]},{"emoji":"👩‍🌾","aliases":["woman_farmer"]},{"emoji":"👩‍🚒","aliases":["woman_firefighter"]},{"emoji":"👩‍⚕️","aliases":["woman_health_worker"]},{"emoji":"👩‍🦽","aliases":["woman_in_manual_wheelchair"]},{"emoji":"👩‍🦼","aliases":["woman_in_motorized_wheelchair"]},{"emoji":"👩‍⚖️","aliases":["woman_judge"]},{"emoji":"🤹‍♀️","aliases":["woman_juggling"]},{"emoji":"👩‍🔧","aliases":["woman_mechanic"]},{"emoji":"👩‍💼","aliases":["woman_office_worker"]},{"emoji":"👩‍✈️","aliases":["woman_pilot"]},{"emoji":"🤾‍♀️","aliases":["woman_playing_handball"]},{"emoji":"🤽‍♀️","aliases":["woman_playing_water_polo"]},{"emoji":"👩‍🔬","aliases":["woman_scientist"]},{"emoji":"🤷‍♀️","aliases":["woman_shrugging"]},{"emoji":"👩‍🎤","aliases":["woman_singer"]},{"emoji":"👩‍🎓","aliases":["woman_student"]},{"emoji":"👩‍🏫","aliases":["woman_teacher"]},{"emoji":"👩‍💻","aliases":["woman_technologist"]},{"emoji":"🧕","aliases":["woman_with_headscarf"]},{"emoji":"👩‍🦯","aliases":["woman_with_probing_cane"]},{"emoji":"👳‍♀️","aliases":["woman_with_turban"]},{"emoji":"👚","aliases":["womans_clothes"]},{"emoji":"👒","aliases":["womans_hat"]},{"emoji":"🤼‍♀️","aliases":["women_wrestling"]},{"emoji":"🚺","aliases":["womens"]},{"emoji":"🥴","aliases":["woozy_face"]},{"emoji":"🗺️","aliases":["world_map"]},{"emoji":"😟","aliases":["worried"]},{"emoji":"🔧","aliases":["wrench"]},{"emoji":"🤼","aliases":["wrestling"]},{"emoji":"✍️","aliases":["writing_hand"]},{"emoji":"❌","aliases":["x"]},{"emoji":"🧶","aliases":["yarn"]},{"emoji":"🥱","aliases":["yawning_face"]},{"emoji":"🟡","aliases":["yellow_circle"]},{"emoji":"💛","aliases":["yellow_heart"]},{"emoji":"🟨","aliases":["yellow_square"]},{"emoji":"🇾🇪","aliases":["yemen"]},{"emoji":"💴","aliases":["yen"]},{"emoji":"☯️","aliases":["yin_yang"]},{"emoji":"🪀","aliases":["yo_yo"]},{"emoji":"😋","aliases":["yum"]},{"emoji":"🇿🇲","aliases":["zambia"]},{"emoji":"🤪","aliases":["zany_face"]},{"emoji":"⚡","aliases":["zap"]},{"emoji":"🦓","aliases":["zebra"]},{"emoji":"0️⃣","aliases":["zero"]},{"emoji":"🇿🇼","aliases":["zimbabwe"]},{"emoji":"🤐","aliases":["zipper_mouth_face"]},{"emoji":"🧟","aliases":["zombie"]},{"emoji":"🧟‍♂️","aliases":["zombie_man"]},{"emoji":"🧟‍♀️","aliases":["zombie_woman"]},{"emoji":"💤","aliases":["zzz"]}] \ No newline at end of file +[{"emoji":"👍","aliases":["+1","thumbsup"]},{"emoji":"👎","aliases":["-1","thumbsdown"]},{"emoji":"💯","aliases":["100"]},{"emoji":"🔢","aliases":["1234"]},{"emoji":"🥇","aliases":["1st_place_medal"]},{"emoji":"🥈","aliases":["2nd_place_medal"]},{"emoji":"🥉","aliases":["3rd_place_medal"]},{"emoji":"🎱","aliases":["8ball"]},{"emoji":"🅰️","aliases":["a"]},{"emoji":"🆎","aliases":["ab"]},{"emoji":"🧮","aliases":["abacus"]},{"emoji":"🔤","aliases":["abc"]},{"emoji":"🔡","aliases":["abcd"]},{"emoji":"🉑","aliases":["accept"]},{"emoji":"🪗","aliases":["accordion"]},{"emoji":"🩹","aliases":["adhesive_bandage"]},{"emoji":"🧑","aliases":["adult"]},{"emoji":"🚡","aliases":["aerial_tramway"]},{"emoji":"🇦🇫","aliases":["afghanistan"]},{"emoji":"✈️","aliases":["airplane"]},{"emoji":"🇦🇽","aliases":["aland_islands"]},{"emoji":"⏰","aliases":["alarm_clock"]},{"emoji":"🇦🇱","aliases":["albania"]},{"emoji":"⚗️","aliases":["alembic"]},{"emoji":"🇩🇿","aliases":["algeria"]},{"emoji":"👽","aliases":["alien"]},{"emoji":"🚑","aliases":["ambulance"]},{"emoji":"🇦🇸","aliases":["american_samoa"]},{"emoji":"🏺","aliases":["amphora"]},{"emoji":"🫀","aliases":["anatomical_heart"]},{"emoji":"⚓","aliases":["anchor"]},{"emoji":"🇦🇩","aliases":["andorra"]},{"emoji":"👼","aliases":["angel"]},{"emoji":"💢","aliases":["anger"]},{"emoji":"🇦🇴","aliases":["angola"]},{"emoji":"😠","aliases":["angry"]},{"emoji":"🇦🇮","aliases":["anguilla"]},{"emoji":"😧","aliases":["anguished"]},{"emoji":"🐜","aliases":["ant"]},{"emoji":"🇦🇶","aliases":["antarctica"]},{"emoji":"🇦🇬","aliases":["antigua_barbuda"]},{"emoji":"🍎","aliases":["apple"]},{"emoji":"♒","aliases":["aquarius"]},{"emoji":"🇦🇷","aliases":["argentina"]},{"emoji":"♈","aliases":["aries"]},{"emoji":"🇦🇲","aliases":["armenia"]},{"emoji":"◀️","aliases":["arrow_backward"]},{"emoji":"⏬","aliases":["arrow_double_down"]},{"emoji":"⏫","aliases":["arrow_double_up"]},{"emoji":"⬇️","aliases":["arrow_down"]},{"emoji":"🔽","aliases":["arrow_down_small"]},{"emoji":"▶️","aliases":["arrow_forward"]},{"emoji":"⤵️","aliases":["arrow_heading_down"]},{"emoji":"⤴️","aliases":["arrow_heading_up"]},{"emoji":"⬅️","aliases":["arrow_left"]},{"emoji":"↙️","aliases":["arrow_lower_left"]},{"emoji":"↘️","aliases":["arrow_lower_right"]},{"emoji":"➡️","aliases":["arrow_right"]},{"emoji":"↪️","aliases":["arrow_right_hook"]},{"emoji":"⬆️","aliases":["arrow_up"]},{"emoji":"↕️","aliases":["arrow_up_down"]},{"emoji":"🔼","aliases":["arrow_up_small"]},{"emoji":"↖️","aliases":["arrow_upper_left"]},{"emoji":"↗️","aliases":["arrow_upper_right"]},{"emoji":"🔃","aliases":["arrows_clockwise"]},{"emoji":"🔄","aliases":["arrows_counterclockwise"]},{"emoji":"🎨","aliases":["art"]},{"emoji":"🚛","aliases":["articulated_lorry"]},{"emoji":"🛰️","aliases":["artificial_satellite"]},{"emoji":"🧑‍🎨","aliases":["artist"]},{"emoji":"🇦🇼","aliases":["aruba"]},{"emoji":"🇦🇨","aliases":["ascension_island"]},{"emoji":"*️⃣","aliases":["asterisk"]},{"emoji":"😲","aliases":["astonished"]},{"emoji":"🧑‍🚀","aliases":["astronaut"]},{"emoji":"👟","aliases":["athletic_shoe"]},{"emoji":"🏧","aliases":["atm"]},{"emoji":"⚛️","aliases":["atom_symbol"]},{"emoji":"🇦🇺","aliases":["australia"]},{"emoji":"🇦🇹","aliases":["austria"]},{"emoji":"🛺","aliases":["auto_rickshaw"]},{"emoji":"🥑","aliases":["avocado"]},{"emoji":"🪓","aliases":["axe"]},{"emoji":"🇦🇿","aliases":["azerbaijan"]},{"emoji":"🅱️","aliases":["b"]},{"emoji":"👶","aliases":["baby"]},{"emoji":"🍼","aliases":["baby_bottle"]},{"emoji":"🐤","aliases":["baby_chick"]},{"emoji":"🚼","aliases":["baby_symbol"]},{"emoji":"🔙","aliases":["back"]},{"emoji":"🥓","aliases":["bacon"]},{"emoji":"🦡","aliases":["badger"]},{"emoji":"🏸","aliases":["badminton"]},{"emoji":"🥯","aliases":["bagel"]},{"emoji":"🛄","aliases":["baggage_claim"]},{"emoji":"🥖","aliases":["baguette_bread"]},{"emoji":"🇧🇸","aliases":["bahamas"]},{"emoji":"🇧🇭","aliases":["bahrain"]},{"emoji":"⚖️","aliases":["balance_scale"]},{"emoji":"👨‍🦲","aliases":["bald_man"]},{"emoji":"👩‍🦲","aliases":["bald_woman"]},{"emoji":"🩰","aliases":["ballet_shoes"]},{"emoji":"🎈","aliases":["balloon"]},{"emoji":"🗳️","aliases":["ballot_box"]},{"emoji":"☑️","aliases":["ballot_box_with_check"]},{"emoji":"🎍","aliases":["bamboo"]},{"emoji":"🍌","aliases":["banana"]},{"emoji":"‼️","aliases":["bangbang"]},{"emoji":"🇧🇩","aliases":["bangladesh"]},{"emoji":"🪕","aliases":["banjo"]},{"emoji":"🏦","aliases":["bank"]},{"emoji":"📊","aliases":["bar_chart"]},{"emoji":"🇧🇧","aliases":["barbados"]},{"emoji":"💈","aliases":["barber"]},{"emoji":"⚾","aliases":["baseball"]},{"emoji":"🧺","aliases":["basket"]},{"emoji":"🏀","aliases":["basketball"]},{"emoji":"🦇","aliases":["bat"]},{"emoji":"🛀","aliases":["bath"]},{"emoji":"🛁","aliases":["bathtub"]},{"emoji":"🔋","aliases":["battery"]},{"emoji":"🏖️","aliases":["beach_umbrella"]},{"emoji":"🫘","aliases":["beans"]},{"emoji":"🐻","aliases":["bear"]},{"emoji":"🧔","aliases":["bearded_person"]},{"emoji":"🦫","aliases":["beaver"]},{"emoji":"🛏️","aliases":["bed"]},{"emoji":"🐝","aliases":["bee","honeybee"]},{"emoji":"🍺","aliases":["beer"]},{"emoji":"🍻","aliases":["beers"]},{"emoji":"🪲","aliases":["beetle"]},{"emoji":"🔰","aliases":["beginner"]},{"emoji":"🇧🇾","aliases":["belarus"]},{"emoji":"🇧🇪","aliases":["belgium"]},{"emoji":"🇧🇿","aliases":["belize"]},{"emoji":"🔔","aliases":["bell"]},{"emoji":"🫑","aliases":["bell_pepper"]},{"emoji":"🛎️","aliases":["bellhop_bell"]},{"emoji":"🇧🇯","aliases":["benin"]},{"emoji":"🍱","aliases":["bento"]},{"emoji":"🇧🇲","aliases":["bermuda"]},{"emoji":"🧃","aliases":["beverage_box"]},{"emoji":"🇧🇹","aliases":["bhutan"]},{"emoji":"🚴","aliases":["bicyclist"]},{"emoji":"🚲","aliases":["bike"]},{"emoji":"🚴‍♂️","aliases":["biking_man"]},{"emoji":"🚴‍♀️","aliases":["biking_woman"]},{"emoji":"👙","aliases":["bikini"]},{"emoji":"🧢","aliases":["billed_cap"]},{"emoji":"☣️","aliases":["biohazard"]},{"emoji":"🐦","aliases":["bird"]},{"emoji":"🎂","aliases":["birthday"]},{"emoji":"🦬","aliases":["bison"]},{"emoji":"🫦","aliases":["biting_lip"]},{"emoji":"🐈‍⬛","aliases":["black_cat"]},{"emoji":"⚫","aliases":["black_circle"]},{"emoji":"🏴","aliases":["black_flag"]},{"emoji":"🖤","aliases":["black_heart"]},{"emoji":"🃏","aliases":["black_joker"]},{"emoji":"⬛","aliases":["black_large_square"]},{"emoji":"◾","aliases":["black_medium_small_square"]},{"emoji":"◼️","aliases":["black_medium_square"]},{"emoji":"✒️","aliases":["black_nib"]},{"emoji":"▪️","aliases":["black_small_square"]},{"emoji":"🔲","aliases":["black_square_button"]},{"emoji":"👱‍♂️","aliases":["blond_haired_man"]},{"emoji":"👱","aliases":["blond_haired_person"]},{"emoji":"👱‍♀️","aliases":["blond_haired_woman","blonde_woman"]},{"emoji":"🌼","aliases":["blossom"]},{"emoji":"🐡","aliases":["blowfish"]},{"emoji":"📘","aliases":["blue_book"]},{"emoji":"🚙","aliases":["blue_car"]},{"emoji":"💙","aliases":["blue_heart"]},{"emoji":"🟦","aliases":["blue_square"]},{"emoji":"🫐","aliases":["blueberries"]},{"emoji":"😊","aliases":["blush"]},{"emoji":"🐗","aliases":["boar"]},{"emoji":"⛵","aliases":["boat","sailboat"]},{"emoji":"🇧🇴","aliases":["bolivia"]},{"emoji":"💣","aliases":["bomb"]},{"emoji":"🦴","aliases":["bone"]},{"emoji":"📖","aliases":["book","open_book"]},{"emoji":"🔖","aliases":["bookmark"]},{"emoji":"📑","aliases":["bookmark_tabs"]},{"emoji":"📚","aliases":["books"]},{"emoji":"💥","aliases":["boom","collision"]},{"emoji":"🪃","aliases":["boomerang"]},{"emoji":"👢","aliases":["boot"]},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"]},{"emoji":"🇧🇼","aliases":["botswana"]},{"emoji":"⛹️‍♂️","aliases":["bouncing_ball_man","basketball_man"]},{"emoji":"⛹️","aliases":["bouncing_ball_person"]},{"emoji":"⛹️‍♀️","aliases":["bouncing_ball_woman","basketball_woman"]},{"emoji":"💐","aliases":["bouquet"]},{"emoji":"🇧🇻","aliases":["bouvet_island"]},{"emoji":"🙇","aliases":["bow"]},{"emoji":"🏹","aliases":["bow_and_arrow"]},{"emoji":"🙇‍♂️","aliases":["bowing_man"]},{"emoji":"🙇‍♀️","aliases":["bowing_woman"]},{"emoji":"🥣","aliases":["bowl_with_spoon"]},{"emoji":"🎳","aliases":["bowling"]},{"emoji":"🥊","aliases":["boxing_glove"]},{"emoji":"👦","aliases":["boy"]},{"emoji":"🧠","aliases":["brain"]},{"emoji":"🇧🇷","aliases":["brazil"]},{"emoji":"🍞","aliases":["bread"]},{"emoji":"🤱","aliases":["breast_feeding"]},{"emoji":"🧱","aliases":["bricks"]},{"emoji":"🌉","aliases":["bridge_at_night"]},{"emoji":"💼","aliases":["briefcase"]},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"]},{"emoji":"🇻🇬","aliases":["british_virgin_islands"]},{"emoji":"🥦","aliases":["broccoli"]},{"emoji":"💔","aliases":["broken_heart"]},{"emoji":"🧹","aliases":["broom"]},{"emoji":"🟤","aliases":["brown_circle"]},{"emoji":"🤎","aliases":["brown_heart"]},{"emoji":"🟫","aliases":["brown_square"]},{"emoji":"🇧🇳","aliases":["brunei"]},{"emoji":"🧋","aliases":["bubble_tea"]},{"emoji":"🫧","aliases":["bubbles"]},{"emoji":"🪣","aliases":["bucket"]},{"emoji":"🐛","aliases":["bug"]},{"emoji":"🏗️","aliases":["building_construction"]},{"emoji":"💡","aliases":["bulb"]},{"emoji":"🇧🇬","aliases":["bulgaria"]},{"emoji":"🚅","aliases":["bullettrain_front"]},{"emoji":"🚄","aliases":["bullettrain_side"]},{"emoji":"🇧🇫","aliases":["burkina_faso"]},{"emoji":"🌯","aliases":["burrito"]},{"emoji":"🇧🇮","aliases":["burundi"]},{"emoji":"🚌","aliases":["bus"]},{"emoji":"🕴️","aliases":["business_suit_levitating"]},{"emoji":"🚏","aliases":["busstop"]},{"emoji":"👤","aliases":["bust_in_silhouette"]},{"emoji":"👥","aliases":["busts_in_silhouette"]},{"emoji":"🧈","aliases":["butter"]},{"emoji":"🦋","aliases":["butterfly"]},{"emoji":"🌵","aliases":["cactus"]},{"emoji":"🍰","aliases":["cake"]},{"emoji":"📆","aliases":["calendar"]},{"emoji":"🤙","aliases":["call_me_hand"]},{"emoji":"📲","aliases":["calling"]},{"emoji":"🇰🇭","aliases":["cambodia"]},{"emoji":"🐫","aliases":["camel"]},{"emoji":"📷","aliases":["camera"]},{"emoji":"📸","aliases":["camera_flash"]},{"emoji":"🇨🇲","aliases":["cameroon"]},{"emoji":"🏕️","aliases":["camping"]},{"emoji":"🇨🇦","aliases":["canada"]},{"emoji":"🇮🇨","aliases":["canary_islands"]},{"emoji":"♋","aliases":["cancer"]},{"emoji":"🕯️","aliases":["candle"]},{"emoji":"🍬","aliases":["candy"]},{"emoji":"🥫","aliases":["canned_food"]},{"emoji":"🛶","aliases":["canoe"]},{"emoji":"🇨🇻","aliases":["cape_verde"]},{"emoji":"🔠","aliases":["capital_abcd"]},{"emoji":"♑","aliases":["capricorn"]},{"emoji":"🚗","aliases":["car","red_car"]},{"emoji":"🗃️","aliases":["card_file_box"]},{"emoji":"📇","aliases":["card_index"]},{"emoji":"🗂️","aliases":["card_index_dividers"]},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"]},{"emoji":"🎠","aliases":["carousel_horse"]},{"emoji":"🪚","aliases":["carpentry_saw"]},{"emoji":"🥕","aliases":["carrot"]},{"emoji":"🤸","aliases":["cartwheeling"]},{"emoji":"🐱","aliases":["cat"]},{"emoji":"🐈","aliases":["cat2"]},{"emoji":"🇰🇾","aliases":["cayman_islands"]},{"emoji":"💿","aliases":["cd"]},{"emoji":"🇨🇫","aliases":["central_african_republic"]},{"emoji":"🇪🇦","aliases":["ceuta_melilla"]},{"emoji":"🇹🇩","aliases":["chad"]},{"emoji":"⛓️","aliases":["chains"]},{"emoji":"🪑","aliases":["chair"]},{"emoji":"🍾","aliases":["champagne"]},{"emoji":"💹","aliases":["chart"]},{"emoji":"📉","aliases":["chart_with_downwards_trend"]},{"emoji":"📈","aliases":["chart_with_upwards_trend"]},{"emoji":"🏁","aliases":["checkered_flag"]},{"emoji":"🧀","aliases":["cheese"]},{"emoji":"🍒","aliases":["cherries"]},{"emoji":"🌸","aliases":["cherry_blossom"]},{"emoji":"♟️","aliases":["chess_pawn"]},{"emoji":"🌰","aliases":["chestnut"]},{"emoji":"🐔","aliases":["chicken"]},{"emoji":"🧒","aliases":["child"]},{"emoji":"🚸","aliases":["children_crossing"]},{"emoji":"🇨🇱","aliases":["chile"]},{"emoji":"🐿️","aliases":["chipmunk"]},{"emoji":"🍫","aliases":["chocolate_bar"]},{"emoji":"🥢","aliases":["chopsticks"]},{"emoji":"🇨🇽","aliases":["christmas_island"]},{"emoji":"🎄","aliases":["christmas_tree"]},{"emoji":"⛪","aliases":["church"]},{"emoji":"🎦","aliases":["cinema"]},{"emoji":"🎪","aliases":["circus_tent"]},{"emoji":"🌇","aliases":["city_sunrise"]},{"emoji":"🌆","aliases":["city_sunset"]},{"emoji":"🏙️","aliases":["cityscape"]},{"emoji":"🆑","aliases":["cl"]},{"emoji":"🗜️","aliases":["clamp"]},{"emoji":"👏","aliases":["clap"]},{"emoji":"🎬","aliases":["clapper"]},{"emoji":"🏛️","aliases":["classical_building"]},{"emoji":"🧗","aliases":["climbing"]},{"emoji":"🧗‍♂️","aliases":["climbing_man"]},{"emoji":"🧗‍♀️","aliases":["climbing_woman"]},{"emoji":"🥂","aliases":["clinking_glasses"]},{"emoji":"📋","aliases":["clipboard"]},{"emoji":"🇨🇵","aliases":["clipperton_island"]},{"emoji":"🕐","aliases":["clock1"]},{"emoji":"🕙","aliases":["clock10"]},{"emoji":"🕥","aliases":["clock1030"]},{"emoji":"🕚","aliases":["clock11"]},{"emoji":"🕦","aliases":["clock1130"]},{"emoji":"🕛","aliases":["clock12"]},{"emoji":"🕧","aliases":["clock1230"]},{"emoji":"🕜","aliases":["clock130"]},{"emoji":"🕑","aliases":["clock2"]},{"emoji":"🕝","aliases":["clock230"]},{"emoji":"🕒","aliases":["clock3"]},{"emoji":"🕞","aliases":["clock330"]},{"emoji":"🕓","aliases":["clock4"]},{"emoji":"🕟","aliases":["clock430"]},{"emoji":"🕔","aliases":["clock5"]},{"emoji":"🕠","aliases":["clock530"]},{"emoji":"🕕","aliases":["clock6"]},{"emoji":"🕡","aliases":["clock630"]},{"emoji":"🕖","aliases":["clock7"]},{"emoji":"🕢","aliases":["clock730"]},{"emoji":"🕗","aliases":["clock8"]},{"emoji":"🕣","aliases":["clock830"]},{"emoji":"🕘","aliases":["clock9"]},{"emoji":"🕤","aliases":["clock930"]},{"emoji":"📕","aliases":["closed_book"]},{"emoji":"🔐","aliases":["closed_lock_with_key"]},{"emoji":"🌂","aliases":["closed_umbrella"]},{"emoji":"☁️","aliases":["cloud"]},{"emoji":"🌩️","aliases":["cloud_with_lightning"]},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"]},{"emoji":"🌧️","aliases":["cloud_with_rain"]},{"emoji":"🌨️","aliases":["cloud_with_snow"]},{"emoji":"🤡","aliases":["clown_face"]},{"emoji":"♣️","aliases":["clubs"]},{"emoji":"🇨🇳","aliases":["cn"]},{"emoji":"🧥","aliases":["coat"]},{"emoji":"🪳","aliases":["cockroach"]},{"emoji":"🍸","aliases":["cocktail"]},{"emoji":"🥥","aliases":["coconut"]},{"emoji":"🇨🇨","aliases":["cocos_islands"]},{"emoji":"☕","aliases":["coffee"]},{"emoji":"⚰️","aliases":["coffin"]},{"emoji":"🪙","aliases":["coin"]},{"emoji":"🥶","aliases":["cold_face"]},{"emoji":"😰","aliases":["cold_sweat"]},{"emoji":"🇨🇴","aliases":["colombia"]},{"emoji":"☄️","aliases":["comet"]},{"emoji":"🇰🇲","aliases":["comoros"]},{"emoji":"🧭","aliases":["compass"]},{"emoji":"💻","aliases":["computer"]},{"emoji":"🖱️","aliases":["computer_mouse"]},{"emoji":"🎊","aliases":["confetti_ball"]},{"emoji":"😖","aliases":["confounded"]},{"emoji":"😕","aliases":["confused"]},{"emoji":"🇨🇬","aliases":["congo_brazzaville"]},{"emoji":"🇨🇩","aliases":["congo_kinshasa"]},{"emoji":"㊗️","aliases":["congratulations"]},{"emoji":"🚧","aliases":["construction"]},{"emoji":"👷","aliases":["construction_worker"]},{"emoji":"👷‍♂️","aliases":["construction_worker_man"]},{"emoji":"👷‍♀️","aliases":["construction_worker_woman"]},{"emoji":"🎛️","aliases":["control_knobs"]},{"emoji":"🏪","aliases":["convenience_store"]},{"emoji":"🧑‍🍳","aliases":["cook"]},{"emoji":"🇨🇰","aliases":["cook_islands"]},{"emoji":"🍪","aliases":["cookie"]},{"emoji":"🆒","aliases":["cool"]},{"emoji":"©️","aliases":["copyright"]},{"emoji":"🪸","aliases":["coral"]},{"emoji":"🌽","aliases":["corn"]},{"emoji":"🇨🇷","aliases":["costa_rica"]},{"emoji":"🇨🇮","aliases":["cote_divoire"]},{"emoji":"🛋️","aliases":["couch_and_lamp"]},{"emoji":"👫","aliases":["couple"]},{"emoji":"💑","aliases":["couple_with_heart"]},{"emoji":"👨‍❤️‍👨","aliases":["couple_with_heart_man_man"]},{"emoji":"👩‍❤️‍👨","aliases":["couple_with_heart_woman_man"]},{"emoji":"👩‍❤️‍👩","aliases":["couple_with_heart_woman_woman"]},{"emoji":"💏","aliases":["couplekiss"]},{"emoji":"👨‍❤️‍💋‍👨","aliases":["couplekiss_man_man"]},{"emoji":"👩‍❤️‍💋‍👨","aliases":["couplekiss_man_woman"]},{"emoji":"👩‍❤️‍💋‍👩","aliases":["couplekiss_woman_woman"]},{"emoji":"🐮","aliases":["cow"]},{"emoji":"🐄","aliases":["cow2"]},{"emoji":"🤠","aliases":["cowboy_hat_face"]},{"emoji":"🦀","aliases":["crab"]},{"emoji":"🖍️","aliases":["crayon"]},{"emoji":"💳","aliases":["credit_card"]},{"emoji":"🌙","aliases":["crescent_moon"]},{"emoji":"🦗","aliases":["cricket"]},{"emoji":"🏏","aliases":["cricket_game"]},{"emoji":"🇭🇷","aliases":["croatia"]},{"emoji":"🐊","aliases":["crocodile"]},{"emoji":"🥐","aliases":["croissant"]},{"emoji":"🤞","aliases":["crossed_fingers"]},{"emoji":"🎌","aliases":["crossed_flags"]},{"emoji":"⚔️","aliases":["crossed_swords"]},{"emoji":"👑","aliases":["crown"]},{"emoji":"🩼","aliases":["crutch"]},{"emoji":"😢","aliases":["cry"]},{"emoji":"😿","aliases":["crying_cat_face"]},{"emoji":"🔮","aliases":["crystal_ball"]},{"emoji":"🇨🇺","aliases":["cuba"]},{"emoji":"🥒","aliases":["cucumber"]},{"emoji":"🥤","aliases":["cup_with_straw"]},{"emoji":"🧁","aliases":["cupcake"]},{"emoji":"💘","aliases":["cupid"]},{"emoji":"🇨🇼","aliases":["curacao"]},{"emoji":"🥌","aliases":["curling_stone"]},{"emoji":"👨‍🦱","aliases":["curly_haired_man"]},{"emoji":"👩‍🦱","aliases":["curly_haired_woman"]},{"emoji":"➰","aliases":["curly_loop"]},{"emoji":"💱","aliases":["currency_exchange"]},{"emoji":"🍛","aliases":["curry"]},{"emoji":"🤬","aliases":["cursing_face"]},{"emoji":"🍮","aliases":["custard"]},{"emoji":"🛃","aliases":["customs"]},{"emoji":"🥩","aliases":["cut_of_meat"]},{"emoji":"🌀","aliases":["cyclone"]},{"emoji":"🇨🇾","aliases":["cyprus"]},{"emoji":"🇨🇿","aliases":["czech_republic"]},{"emoji":"🗡️","aliases":["dagger"]},{"emoji":"👯","aliases":["dancers"]},{"emoji":"👯‍♂️","aliases":["dancing_men"]},{"emoji":"👯‍♀️","aliases":["dancing_women"]},{"emoji":"🍡","aliases":["dango"]},{"emoji":"🕶️","aliases":["dark_sunglasses"]},{"emoji":"🎯","aliases":["dart"]},{"emoji":"💨","aliases":["dash"]},{"emoji":"📅","aliases":["date"]},{"emoji":"🇩🇪","aliases":["de"]},{"emoji":"🧏‍♂️","aliases":["deaf_man"]},{"emoji":"🧏","aliases":["deaf_person"]},{"emoji":"🧏‍♀️","aliases":["deaf_woman"]},{"emoji":"🌳","aliases":["deciduous_tree"]},{"emoji":"🦌","aliases":["deer"]},{"emoji":"🇩🇰","aliases":["denmark"]},{"emoji":"🏬","aliases":["department_store"]},{"emoji":"🏚️","aliases":["derelict_house"]},{"emoji":"🏜️","aliases":["desert"]},{"emoji":"🏝️","aliases":["desert_island"]},{"emoji":"🖥️","aliases":["desktop_computer"]},{"emoji":"🕵️","aliases":["detective"]},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"]},{"emoji":"♦️","aliases":["diamonds"]},{"emoji":"🇩🇬","aliases":["diego_garcia"]},{"emoji":"😞","aliases":["disappointed"]},{"emoji":"😥","aliases":["disappointed_relieved"]},{"emoji":"🥸","aliases":["disguised_face"]},{"emoji":"🤿","aliases":["diving_mask"]},{"emoji":"🪔","aliases":["diya_lamp"]},{"emoji":"💫","aliases":["dizzy"]},{"emoji":"😵","aliases":["dizzy_face"]},{"emoji":"🇩🇯","aliases":["djibouti"]},{"emoji":"🧬","aliases":["dna"]},{"emoji":"🚯","aliases":["do_not_litter"]},{"emoji":"🦤","aliases":["dodo"]},{"emoji":"🐶","aliases":["dog"]},{"emoji":"🐕","aliases":["dog2"]},{"emoji":"💵","aliases":["dollar"]},{"emoji":"🎎","aliases":["dolls"]},{"emoji":"🐬","aliases":["dolphin","flipper"]},{"emoji":"🇩🇲","aliases":["dominica"]},{"emoji":"🇩🇴","aliases":["dominican_republic"]},{"emoji":"🚪","aliases":["door"]},{"emoji":"🫥","aliases":["dotted_line_face"]},{"emoji":"🍩","aliases":["doughnut"]},{"emoji":"🕊️","aliases":["dove"]},{"emoji":"🐉","aliases":["dragon"]},{"emoji":"🐲","aliases":["dragon_face"]},{"emoji":"👗","aliases":["dress"]},{"emoji":"🐪","aliases":["dromedary_camel"]},{"emoji":"🤤","aliases":["drooling_face"]},{"emoji":"🩸","aliases":["drop_of_blood"]},{"emoji":"💧","aliases":["droplet"]},{"emoji":"🥁","aliases":["drum"]},{"emoji":"🦆","aliases":["duck"]},{"emoji":"🥟","aliases":["dumpling"]},{"emoji":"📀","aliases":["dvd"]},{"emoji":"🦅","aliases":["eagle"]},{"emoji":"👂","aliases":["ear"]},{"emoji":"🌾","aliases":["ear_of_rice"]},{"emoji":"🦻","aliases":["ear_with_hearing_aid"]},{"emoji":"🌍","aliases":["earth_africa"]},{"emoji":"🌎","aliases":["earth_americas"]},{"emoji":"🌏","aliases":["earth_asia"]},{"emoji":"🇪🇨","aliases":["ecuador"]},{"emoji":"🥚","aliases":["egg"]},{"emoji":"🍆","aliases":["eggplant"]},{"emoji":"🇪🇬","aliases":["egypt"]},{"emoji":"8️⃣","aliases":["eight"]},{"emoji":"✴️","aliases":["eight_pointed_black_star"]},{"emoji":"✳️","aliases":["eight_spoked_asterisk"]},{"emoji":"⏏️","aliases":["eject_button"]},{"emoji":"🇸🇻","aliases":["el_salvador"]},{"emoji":"🔌","aliases":["electric_plug"]},{"emoji":"🐘","aliases":["elephant"]},{"emoji":"🛗","aliases":["elevator"]},{"emoji":"🧝","aliases":["elf"]},{"emoji":"🧝‍♂️","aliases":["elf_man"]},{"emoji":"🧝‍♀️","aliases":["elf_woman"]},{"emoji":"📧","aliases":["email","e-mail"]},{"emoji":"🪹","aliases":["empty_nest"]},{"emoji":"🔚","aliases":["end"]},{"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","aliases":["england"]},{"emoji":"✉️","aliases":["envelope"]},{"emoji":"📩","aliases":["envelope_with_arrow"]},{"emoji":"🇬🇶","aliases":["equatorial_guinea"]},{"emoji":"🇪🇷","aliases":["eritrea"]},{"emoji":"🇪🇸","aliases":["es"]},{"emoji":"🇪🇪","aliases":["estonia"]},{"emoji":"🇪🇹","aliases":["ethiopia"]},{"emoji":"🇪🇺","aliases":["eu","european_union"]},{"emoji":"💶","aliases":["euro"]},{"emoji":"🏰","aliases":["european_castle"]},{"emoji":"🏤","aliases":["european_post_office"]},{"emoji":"🌲","aliases":["evergreen_tree"]},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"]},{"emoji":"🤯","aliases":["exploding_head"]},{"emoji":"😑","aliases":["expressionless"]},{"emoji":"👁️","aliases":["eye"]},{"emoji":"👁️‍🗨️","aliases":["eye_speech_bubble"]},{"emoji":"👓","aliases":["eyeglasses"]},{"emoji":"👀","aliases":["eyes"]},{"emoji":"😮‍💨","aliases":["face_exhaling"]},{"emoji":"🥹","aliases":["face_holding_back_tears"]},{"emoji":"😶‍🌫️","aliases":["face_in_clouds"]},{"emoji":"🫤","aliases":["face_with_diagonal_mouth"]},{"emoji":"🤕","aliases":["face_with_head_bandage"]},{"emoji":"🫢","aliases":["face_with_open_eyes_and_hand_over_mouth"]},{"emoji":"🫣","aliases":["face_with_peeking_eye"]},{"emoji":"😵‍💫","aliases":["face_with_spiral_eyes"]},{"emoji":"🤒","aliases":["face_with_thermometer"]},{"emoji":"🤦","aliases":["facepalm"]},{"emoji":"🏭","aliases":["factory"]},{"emoji":"🧑‍🏭","aliases":["factory_worker"]},{"emoji":"🧚","aliases":["fairy"]},{"emoji":"🧚‍♂️","aliases":["fairy_man"]},{"emoji":"🧚‍♀️","aliases":["fairy_woman"]},{"emoji":"🧆","aliases":["falafel"]},{"emoji":"🇫🇰","aliases":["falkland_islands"]},{"emoji":"🍂","aliases":["fallen_leaf"]},{"emoji":"👪","aliases":["family"]},{"emoji":"👨‍👦","aliases":["family_man_boy"]},{"emoji":"👨‍👦‍👦","aliases":["family_man_boy_boy"]},{"emoji":"👨‍👧","aliases":["family_man_girl"]},{"emoji":"👨‍👧‍👦","aliases":["family_man_girl_boy"]},{"emoji":"👨‍👧‍👧","aliases":["family_man_girl_girl"]},{"emoji":"👨‍👨‍👦","aliases":["family_man_man_boy"]},{"emoji":"👨‍👨‍👦‍👦","aliases":["family_man_man_boy_boy"]},{"emoji":"👨‍👨‍👧","aliases":["family_man_man_girl"]},{"emoji":"👨‍👨‍👧‍👦","aliases":["family_man_man_girl_boy"]},{"emoji":"👨‍👨‍👧‍👧","aliases":["family_man_man_girl_girl"]},{"emoji":"👨‍👩‍👦","aliases":["family_man_woman_boy"]},{"emoji":"👨‍👩‍👦‍👦","aliases":["family_man_woman_boy_boy"]},{"emoji":"👨‍👩‍👧","aliases":["family_man_woman_girl"]},{"emoji":"👨‍👩‍👧‍👦","aliases":["family_man_woman_girl_boy"]},{"emoji":"👨‍👩‍👧‍👧","aliases":["family_man_woman_girl_girl"]},{"emoji":"👩‍👦","aliases":["family_woman_boy"]},{"emoji":"👩‍👦‍👦","aliases":["family_woman_boy_boy"]},{"emoji":"👩‍👧","aliases":["family_woman_girl"]},{"emoji":"👩‍👧‍👦","aliases":["family_woman_girl_boy"]},{"emoji":"👩‍👧‍👧","aliases":["family_woman_girl_girl"]},{"emoji":"👩‍👩‍👦","aliases":["family_woman_woman_boy"]},{"emoji":"👩‍👩‍👦‍👦","aliases":["family_woman_woman_boy_boy"]},{"emoji":"👩‍👩‍👧","aliases":["family_woman_woman_girl"]},{"emoji":"👩‍👩‍👧‍👦","aliases":["family_woman_woman_girl_boy"]},{"emoji":"👩‍👩‍👧‍👧","aliases":["family_woman_woman_girl_girl"]},{"emoji":"🧑‍🌾","aliases":["farmer"]},{"emoji":"🇫🇴","aliases":["faroe_islands"]},{"emoji":"⏩","aliases":["fast_forward"]},{"emoji":"📠","aliases":["fax"]},{"emoji":"😨","aliases":["fearful"]},{"emoji":"🪶","aliases":["feather"]},{"emoji":"🐾","aliases":["feet","paw_prints"]},{"emoji":"🕵️‍♀️","aliases":["female_detective"]},{"emoji":"♀️","aliases":["female_sign"]},{"emoji":"🎡","aliases":["ferris_wheel"]},{"emoji":"⛴️","aliases":["ferry"]},{"emoji":"🏑","aliases":["field_hockey"]},{"emoji":"🇫🇯","aliases":["fiji"]},{"emoji":"🗄️","aliases":["file_cabinet"]},{"emoji":"📁","aliases":["file_folder"]},{"emoji":"📽️","aliases":["film_projector"]},{"emoji":"🎞️","aliases":["film_strip"]},{"emoji":"🇫🇮","aliases":["finland"]},{"emoji":"🔥","aliases":["fire"]},{"emoji":"🚒","aliases":["fire_engine"]},{"emoji":"🧯","aliases":["fire_extinguisher"]},{"emoji":"🧨","aliases":["firecracker"]},{"emoji":"🧑‍🚒","aliases":["firefighter"]},{"emoji":"🎆","aliases":["fireworks"]},{"emoji":"🌓","aliases":["first_quarter_moon"]},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"]},{"emoji":"🐟","aliases":["fish"]},{"emoji":"🍥","aliases":["fish_cake"]},{"emoji":"🎣","aliases":["fishing_pole_and_fish"]},{"emoji":"🤛","aliases":["fist_left"]},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"]},{"emoji":"✊","aliases":["fist_raised","fist"]},{"emoji":"🤜","aliases":["fist_right"]},{"emoji":"5️⃣","aliases":["five"]},{"emoji":"🎏","aliases":["flags"]},{"emoji":"🦩","aliases":["flamingo"]},{"emoji":"🔦","aliases":["flashlight"]},{"emoji":"🥿","aliases":["flat_shoe"]},{"emoji":"🫓","aliases":["flatbread"]},{"emoji":"⚜️","aliases":["fleur_de_lis"]},{"emoji":"🛬","aliases":["flight_arrival"]},{"emoji":"🛫","aliases":["flight_departure"]},{"emoji":"💾","aliases":["floppy_disk"]},{"emoji":"🎴","aliases":["flower_playing_cards"]},{"emoji":"😳","aliases":["flushed"]},{"emoji":"🪰","aliases":["fly"]},{"emoji":"🥏","aliases":["flying_disc"]},{"emoji":"🛸","aliases":["flying_saucer"]},{"emoji":"🌫️","aliases":["fog"]},{"emoji":"🌁","aliases":["foggy"]},{"emoji":"🫕","aliases":["fondue"]},{"emoji":"🦶","aliases":["foot"]},{"emoji":"🏈","aliases":["football"]},{"emoji":"👣","aliases":["footprints"]},{"emoji":"🍴","aliases":["fork_and_knife"]},{"emoji":"🥠","aliases":["fortune_cookie"]},{"emoji":"⛲","aliases":["fountain"]},{"emoji":"🖋️","aliases":["fountain_pen"]},{"emoji":"4️⃣","aliases":["four"]},{"emoji":"🍀","aliases":["four_leaf_clover"]},{"emoji":"🦊","aliases":["fox_face"]},{"emoji":"🇫🇷","aliases":["fr"]},{"emoji":"🖼️","aliases":["framed_picture"]},{"emoji":"🆓","aliases":["free"]},{"emoji":"🇬🇫","aliases":["french_guiana"]},{"emoji":"🇵🇫","aliases":["french_polynesia"]},{"emoji":"🇹🇫","aliases":["french_southern_territories"]},{"emoji":"🍳","aliases":["fried_egg"]},{"emoji":"🍤","aliases":["fried_shrimp"]},{"emoji":"🍟","aliases":["fries"]},{"emoji":"🐸","aliases":["frog"]},{"emoji":"😦","aliases":["frowning"]},{"emoji":"☹️","aliases":["frowning_face"]},{"emoji":"🙍‍♂️","aliases":["frowning_man"]},{"emoji":"🙍","aliases":["frowning_person"]},{"emoji":"🙍‍♀️","aliases":["frowning_woman"]},{"emoji":"⛽","aliases":["fuelpump"]},{"emoji":"🌕","aliases":["full_moon"]},{"emoji":"🌝","aliases":["full_moon_with_face"]},{"emoji":"⚱️","aliases":["funeral_urn"]},{"emoji":"🇬🇦","aliases":["gabon"]},{"emoji":"🇬🇲","aliases":["gambia"]},{"emoji":"🎲","aliases":["game_die"]},{"emoji":"🧄","aliases":["garlic"]},{"emoji":"🇬🇧","aliases":["gb","uk"]},{"emoji":"⚙️","aliases":["gear"]},{"emoji":"💎","aliases":["gem"]},{"emoji":"♊","aliases":["gemini"]},{"emoji":"🧞","aliases":["genie"]},{"emoji":"🧞‍♂️","aliases":["genie_man"]},{"emoji":"🧞‍♀️","aliases":["genie_woman"]},{"emoji":"🇬🇪","aliases":["georgia"]},{"emoji":"🇬🇭","aliases":["ghana"]},{"emoji":"👻","aliases":["ghost"]},{"emoji":"🇬🇮","aliases":["gibraltar"]},{"emoji":"🎁","aliases":["gift"]},{"emoji":"💝","aliases":["gift_heart"]},{"emoji":"🦒","aliases":["giraffe"]},{"emoji":"👧","aliases":["girl"]},{"emoji":"🌐","aliases":["globe_with_meridians"]},{"emoji":"🧤","aliases":["gloves"]},{"emoji":"🥅","aliases":["goal_net"]},{"emoji":"🐐","aliases":["goat"]},{"emoji":"🥽","aliases":["goggles"]},{"emoji":"⛳","aliases":["golf"]},{"emoji":"🏌️","aliases":["golfing"]},{"emoji":"🏌️‍♂️","aliases":["golfing_man"]},{"emoji":"🏌️‍♀️","aliases":["golfing_woman"]},{"emoji":"🦍","aliases":["gorilla"]},{"emoji":"🍇","aliases":["grapes"]},{"emoji":"🇬🇷","aliases":["greece"]},{"emoji":"🍏","aliases":["green_apple"]},{"emoji":"📗","aliases":["green_book"]},{"emoji":"🟢","aliases":["green_circle"]},{"emoji":"💚","aliases":["green_heart"]},{"emoji":"🥗","aliases":["green_salad"]},{"emoji":"🟩","aliases":["green_square"]},{"emoji":"🇬🇱","aliases":["greenland"]},{"emoji":"🇬🇩","aliases":["grenada"]},{"emoji":"❕","aliases":["grey_exclamation"]},{"emoji":"❔","aliases":["grey_question"]},{"emoji":"😬","aliases":["grimacing"]},{"emoji":"😁","aliases":["grin"]},{"emoji":"😀","aliases":["grinning"]},{"emoji":"🇬🇵","aliases":["guadeloupe"]},{"emoji":"🇬🇺","aliases":["guam"]},{"emoji":"💂","aliases":["guard"]},{"emoji":"💂‍♂️","aliases":["guardsman"]},{"emoji":"💂‍♀️","aliases":["guardswoman"]},{"emoji":"🇬🇹","aliases":["guatemala"]},{"emoji":"🇬🇬","aliases":["guernsey"]},{"emoji":"🦮","aliases":["guide_dog"]},{"emoji":"🇬🇳","aliases":["guinea"]},{"emoji":"🇬🇼","aliases":["guinea_bissau"]},{"emoji":"🎸","aliases":["guitar"]},{"emoji":"🔫","aliases":["gun"]},{"emoji":"🇬🇾","aliases":["guyana"]},{"emoji":"💇","aliases":["haircut"]},{"emoji":"💇‍♂️","aliases":["haircut_man"]},{"emoji":"💇‍♀️","aliases":["haircut_woman"]},{"emoji":"🇭🇹","aliases":["haiti"]},{"emoji":"🍔","aliases":["hamburger"]},{"emoji":"🔨","aliases":["hammer"]},{"emoji":"⚒️","aliases":["hammer_and_pick"]},{"emoji":"🛠️","aliases":["hammer_and_wrench"]},{"emoji":"🪬","aliases":["hamsa"]},{"emoji":"🐹","aliases":["hamster"]},{"emoji":"✋","aliases":["hand","raised_hand"]},{"emoji":"🤭","aliases":["hand_over_mouth"]},{"emoji":"🫰","aliases":["hand_with_index_finger_and_thumb_crossed"]},{"emoji":"👜","aliases":["handbag"]},{"emoji":"🤾","aliases":["handball_person"]},{"emoji":"🤝","aliases":["handshake"]},{"emoji":"💩","aliases":["hankey","poop","shit"]},{"emoji":"#️⃣","aliases":["hash"]},{"emoji":"🐥","aliases":["hatched_chick"]},{"emoji":"🐣","aliases":["hatching_chick"]},{"emoji":"🎧","aliases":["headphones"]},{"emoji":"🪦","aliases":["headstone"]},{"emoji":"🧑‍⚕️","aliases":["health_worker"]},{"emoji":"🙉","aliases":["hear_no_evil"]},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"]},{"emoji":"❤️","aliases":["heart"]},{"emoji":"💟","aliases":["heart_decoration"]},{"emoji":"😍","aliases":["heart_eyes"]},{"emoji":"😻","aliases":["heart_eyes_cat"]},{"emoji":"🫶","aliases":["heart_hands"]},{"emoji":"❤️‍🔥","aliases":["heart_on_fire"]},{"emoji":"💓","aliases":["heartbeat"]},{"emoji":"💗","aliases":["heartpulse"]},{"emoji":"♥️","aliases":["hearts"]},{"emoji":"✔️","aliases":["heavy_check_mark"]},{"emoji":"➗","aliases":["heavy_division_sign"]},{"emoji":"💲","aliases":["heavy_dollar_sign"]},{"emoji":"🟰","aliases":["heavy_equals_sign"]},{"emoji":"❣️","aliases":["heavy_heart_exclamation"]},{"emoji":"➖","aliases":["heavy_minus_sign"]},{"emoji":"✖️","aliases":["heavy_multiplication_x"]},{"emoji":"➕","aliases":["heavy_plus_sign"]},{"emoji":"🦔","aliases":["hedgehog"]},{"emoji":"🚁","aliases":["helicopter"]},{"emoji":"🌿","aliases":["herb"]},{"emoji":"🌺","aliases":["hibiscus"]},{"emoji":"🔆","aliases":["high_brightness"]},{"emoji":"👠","aliases":["high_heel"]},{"emoji":"🥾","aliases":["hiking_boot"]},{"emoji":"🛕","aliases":["hindu_temple"]},{"emoji":"🦛","aliases":["hippopotamus"]},{"emoji":"🔪","aliases":["hocho","knife"]},{"emoji":"🕳️","aliases":["hole"]},{"emoji":"🇭🇳","aliases":["honduras"]},{"emoji":"🍯","aliases":["honey_pot"]},{"emoji":"🇭🇰","aliases":["hong_kong"]},{"emoji":"🪝","aliases":["hook"]},{"emoji":"🐴","aliases":["horse"]},{"emoji":"🏇","aliases":["horse_racing"]},{"emoji":"🏥","aliases":["hospital"]},{"emoji":"🥵","aliases":["hot_face"]},{"emoji":"🌶️","aliases":["hot_pepper"]},{"emoji":"🌭","aliases":["hotdog"]},{"emoji":"🏨","aliases":["hotel"]},{"emoji":"♨️","aliases":["hotsprings"]},{"emoji":"⌛","aliases":["hourglass"]},{"emoji":"⏳","aliases":["hourglass_flowing_sand"]},{"emoji":"🏠","aliases":["house"]},{"emoji":"🏡","aliases":["house_with_garden"]},{"emoji":"🏘️","aliases":["houses"]},{"emoji":"🤗","aliases":["hugs"]},{"emoji":"🇭🇺","aliases":["hungary"]},{"emoji":"😯","aliases":["hushed"]},{"emoji":"🛖","aliases":["hut"]},{"emoji":"🍨","aliases":["ice_cream"]},{"emoji":"🧊","aliases":["ice_cube"]},{"emoji":"🏒","aliases":["ice_hockey"]},{"emoji":"⛸️","aliases":["ice_skate"]},{"emoji":"🍦","aliases":["icecream"]},{"emoji":"🇮🇸","aliases":["iceland"]},{"emoji":"🆔","aliases":["id"]},{"emoji":"🪪","aliases":["identification_card"]},{"emoji":"🉐","aliases":["ideograph_advantage"]},{"emoji":"👿","aliases":["imp"]},{"emoji":"📥","aliases":["inbox_tray"]},{"emoji":"📨","aliases":["incoming_envelope"]},{"emoji":"🫵","aliases":["index_pointing_at_the_viewer"]},{"emoji":"🇮🇳","aliases":["india"]},{"emoji":"🇮🇩","aliases":["indonesia"]},{"emoji":"♾️","aliases":["infinity"]},{"emoji":"ℹ️","aliases":["information_source"]},{"emoji":"😇","aliases":["innocent"]},{"emoji":"⁉️","aliases":["interrobang"]},{"emoji":"📱","aliases":["iphone"]},{"emoji":"🇮🇷","aliases":["iran"]},{"emoji":"🇮🇶","aliases":["iraq"]},{"emoji":"🇮🇪","aliases":["ireland"]},{"emoji":"🇮🇲","aliases":["isle_of_man"]},{"emoji":"🇮🇱","aliases":["israel"]},{"emoji":"🇮🇹","aliases":["it"]},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"]},{"emoji":"🎃","aliases":["jack_o_lantern"]},{"emoji":"🇯🇲","aliases":["jamaica"]},{"emoji":"🗾","aliases":["japan"]},{"emoji":"🏯","aliases":["japanese_castle"]},{"emoji":"👺","aliases":["japanese_goblin"]},{"emoji":"👹","aliases":["japanese_ogre"]},{"emoji":"🫙","aliases":["jar"]},{"emoji":"👖","aliases":["jeans"]},{"emoji":"🇯🇪","aliases":["jersey"]},{"emoji":"🧩","aliases":["jigsaw"]},{"emoji":"🇯🇴","aliases":["jordan"]},{"emoji":"😂","aliases":["joy"]},{"emoji":"😹","aliases":["joy_cat"]},{"emoji":"🕹️","aliases":["joystick"]},{"emoji":"🇯🇵","aliases":["jp"]},{"emoji":"🧑‍⚖️","aliases":["judge"]},{"emoji":"🤹","aliases":["juggling_person"]},{"emoji":"🕋","aliases":["kaaba"]},{"emoji":"🦘","aliases":["kangaroo"]},{"emoji":"🇰🇿","aliases":["kazakhstan"]},{"emoji":"🇰🇪","aliases":["kenya"]},{"emoji":"🔑","aliases":["key"]},{"emoji":"⌨️","aliases":["keyboard"]},{"emoji":"🔟","aliases":["keycap_ten"]},{"emoji":"🛴","aliases":["kick_scooter"]},{"emoji":"👘","aliases":["kimono"]},{"emoji":"🇰🇮","aliases":["kiribati"]},{"emoji":"💋","aliases":["kiss"]},{"emoji":"😗","aliases":["kissing"]},{"emoji":"😽","aliases":["kissing_cat"]},{"emoji":"😚","aliases":["kissing_closed_eyes"]},{"emoji":"😘","aliases":["kissing_heart"]},{"emoji":"😙","aliases":["kissing_smiling_eyes"]},{"emoji":"🪁","aliases":["kite"]},{"emoji":"🥝","aliases":["kiwi_fruit"]},{"emoji":"🧎‍♂️","aliases":["kneeling_man"]},{"emoji":"🧎","aliases":["kneeling_person"]},{"emoji":"🧎‍♀️","aliases":["kneeling_woman"]},{"emoji":"🪢","aliases":["knot"]},{"emoji":"🐨","aliases":["koala"]},{"emoji":"🈁","aliases":["koko"]},{"emoji":"🇽🇰","aliases":["kosovo"]},{"emoji":"🇰🇷","aliases":["kr"]},{"emoji":"🇰🇼","aliases":["kuwait"]},{"emoji":"🇰🇬","aliases":["kyrgyzstan"]},{"emoji":"🥼","aliases":["lab_coat"]},{"emoji":"🏷️","aliases":["label"]},{"emoji":"🥍","aliases":["lacrosse"]},{"emoji":"🪜","aliases":["ladder"]},{"emoji":"🐞","aliases":["lady_beetle"]},{"emoji":"🇱🇦","aliases":["laos"]},{"emoji":"🔵","aliases":["large_blue_circle"]},{"emoji":"🔷","aliases":["large_blue_diamond"]},{"emoji":"🔶","aliases":["large_orange_diamond"]},{"emoji":"🌗","aliases":["last_quarter_moon"]},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"]},{"emoji":"✝️","aliases":["latin_cross"]},{"emoji":"🇱🇻","aliases":["latvia"]},{"emoji":"😆","aliases":["laughing","satisfied","laugh"]},{"emoji":"🥬","aliases":["leafy_green"]},{"emoji":"🍃","aliases":["leaves"]},{"emoji":"🇱🇧","aliases":["lebanon"]},{"emoji":"📒","aliases":["ledger"]},{"emoji":"🛅","aliases":["left_luggage"]},{"emoji":"↔️","aliases":["left_right_arrow"]},{"emoji":"🗨️","aliases":["left_speech_bubble"]},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"]},{"emoji":"🫲","aliases":["leftwards_hand"]},{"emoji":"🦵","aliases":["leg"]},{"emoji":"🍋","aliases":["lemon"]},{"emoji":"♌","aliases":["leo"]},{"emoji":"🐆","aliases":["leopard"]},{"emoji":"🇱🇸","aliases":["lesotho"]},{"emoji":"🎚️","aliases":["level_slider"]},{"emoji":"🇱🇷","aliases":["liberia"]},{"emoji":"♎","aliases":["libra"]},{"emoji":"🇱🇾","aliases":["libya"]},{"emoji":"🇱🇮","aliases":["liechtenstein"]},{"emoji":"🚈","aliases":["light_rail"]},{"emoji":"🔗","aliases":["link"]},{"emoji":"🦁","aliases":["lion"]},{"emoji":"👄","aliases":["lips"]},{"emoji":"💄","aliases":["lipstick"]},{"emoji":"🇱🇹","aliases":["lithuania"]},{"emoji":"🦎","aliases":["lizard"]},{"emoji":"🦙","aliases":["llama"]},{"emoji":"🦞","aliases":["lobster"]},{"emoji":"🔒","aliases":["lock"]},{"emoji":"🔏","aliases":["lock_with_ink_pen"]},{"emoji":"🍭","aliases":["lollipop"]},{"emoji":"🪘","aliases":["long_drum"]},{"emoji":"➿","aliases":["loop"]},{"emoji":"🧴","aliases":["lotion_bottle"]},{"emoji":"🪷","aliases":["lotus"]},{"emoji":"🧘","aliases":["lotus_position"]},{"emoji":"🧘‍♂️","aliases":["lotus_position_man"]},{"emoji":"🧘‍♀️","aliases":["lotus_position_woman"]},{"emoji":"🔊","aliases":["loud_sound"]},{"emoji":"📢","aliases":["loudspeaker"]},{"emoji":"🏩","aliases":["love_hotel"]},{"emoji":"💌","aliases":["love_letter"]},{"emoji":"🤟","aliases":["love_you_gesture"]},{"emoji":"🪫","aliases":["low_battery"]},{"emoji":"🔅","aliases":["low_brightness"]},{"emoji":"🧳","aliases":["luggage"]},{"emoji":"🫁","aliases":["lungs"]},{"emoji":"🇱🇺","aliases":["luxembourg"]},{"emoji":"🤥","aliases":["lying_face"]},{"emoji":"Ⓜ️","aliases":["m"]},{"emoji":"🇲🇴","aliases":["macau"]},{"emoji":"🇲🇰","aliases":["macedonia"]},{"emoji":"🇲🇬","aliases":["madagascar"]},{"emoji":"🔍","aliases":["mag"]},{"emoji":"🔎","aliases":["mag_right"]},{"emoji":"🧙","aliases":["mage"]},{"emoji":"🧙‍♂️","aliases":["mage_man"]},{"emoji":"🧙‍♀️","aliases":["mage_woman"]},{"emoji":"🪄","aliases":["magic_wand"]},{"emoji":"🧲","aliases":["magnet"]},{"emoji":"🀄","aliases":["mahjong"]},{"emoji":"📫","aliases":["mailbox"]},{"emoji":"📪","aliases":["mailbox_closed"]},{"emoji":"📬","aliases":["mailbox_with_mail"]},{"emoji":"📭","aliases":["mailbox_with_no_mail"]},{"emoji":"🇲🇼","aliases":["malawi"]},{"emoji":"🇲🇾","aliases":["malaysia"]},{"emoji":"🇲🇻","aliases":["maldives"]},{"emoji":"🕵️‍♂️","aliases":["male_detective"]},{"emoji":"♂️","aliases":["male_sign"]},{"emoji":"🇲🇱","aliases":["mali"]},{"emoji":"🇲🇹","aliases":["malta"]},{"emoji":"🦣","aliases":["mammoth"]},{"emoji":"👨","aliases":["man"]},{"emoji":"👨‍🎨","aliases":["man_artist"]},{"emoji":"👨‍🚀","aliases":["man_astronaut"]},{"emoji":"🧔‍♂️","aliases":["man_beard"]},{"emoji":"🤸‍♂️","aliases":["man_cartwheeling"]},{"emoji":"👨‍🍳","aliases":["man_cook"]},{"emoji":"🕺","aliases":["man_dancing"]},{"emoji":"🤦‍♂️","aliases":["man_facepalming"]},{"emoji":"👨‍🏭","aliases":["man_factory_worker"]},{"emoji":"👨‍🌾","aliases":["man_farmer"]},{"emoji":"👨‍🍼","aliases":["man_feeding_baby"]},{"emoji":"👨‍🚒","aliases":["man_firefighter"]},{"emoji":"👨‍⚕️","aliases":["man_health_worker"]},{"emoji":"👨‍🦽","aliases":["man_in_manual_wheelchair"]},{"emoji":"👨‍🦼","aliases":["man_in_motorized_wheelchair"]},{"emoji":"🤵‍♂️","aliases":["man_in_tuxedo"]},{"emoji":"👨‍⚖️","aliases":["man_judge"]},{"emoji":"🤹‍♂️","aliases":["man_juggling"]},{"emoji":"👨‍🔧","aliases":["man_mechanic"]},{"emoji":"👨‍💼","aliases":["man_office_worker"]},{"emoji":"👨‍✈️","aliases":["man_pilot"]},{"emoji":"🤾‍♂️","aliases":["man_playing_handball"]},{"emoji":"🤽‍♂️","aliases":["man_playing_water_polo"]},{"emoji":"👨‍🔬","aliases":["man_scientist"]},{"emoji":"🤷‍♂️","aliases":["man_shrugging"]},{"emoji":"👨‍🎤","aliases":["man_singer"]},{"emoji":"👨‍🎓","aliases":["man_student"]},{"emoji":"👨‍🏫","aliases":["man_teacher"]},{"emoji":"👨‍💻","aliases":["man_technologist"]},{"emoji":"👲","aliases":["man_with_gua_pi_mao"]},{"emoji":"👨‍🦯","aliases":["man_with_probing_cane"]},{"emoji":"👳‍♂️","aliases":["man_with_turban"]},{"emoji":"👰‍♂️","aliases":["man_with_veil"]},{"emoji":"🥭","aliases":["mango"]},{"emoji":"👞","aliases":["mans_shoe","shoe"]},{"emoji":"🕰️","aliases":["mantelpiece_clock"]},{"emoji":"🦽","aliases":["manual_wheelchair"]},{"emoji":"🍁","aliases":["maple_leaf"]},{"emoji":"🇲🇭","aliases":["marshall_islands"]},{"emoji":"🥋","aliases":["martial_arts_uniform"]},{"emoji":"🇲🇶","aliases":["martinique"]},{"emoji":"😷","aliases":["mask"]},{"emoji":"💆","aliases":["massage"]},{"emoji":"💆‍♂️","aliases":["massage_man"]},{"emoji":"💆‍♀️","aliases":["massage_woman"]},{"emoji":"🧉","aliases":["mate"]},{"emoji":"🇲🇷","aliases":["mauritania"]},{"emoji":"🇲🇺","aliases":["mauritius"]},{"emoji":"🇾🇹","aliases":["mayotte"]},{"emoji":"🍖","aliases":["meat_on_bone"]},{"emoji":"🧑‍🔧","aliases":["mechanic"]},{"emoji":"🦾","aliases":["mechanical_arm"]},{"emoji":"🦿","aliases":["mechanical_leg"]},{"emoji":"🎖️","aliases":["medal_military"]},{"emoji":"🏅","aliases":["medal_sports"]},{"emoji":"⚕️","aliases":["medical_symbol"]},{"emoji":"📣","aliases":["mega"]},{"emoji":"🍈","aliases":["melon"]},{"emoji":"🫠","aliases":["melting_face"]},{"emoji":"📝","aliases":["memo","pencil"]},{"emoji":"🤼‍♂️","aliases":["men_wrestling"]},{"emoji":"❤️‍🩹","aliases":["mending_heart"]},{"emoji":"🕎","aliases":["menorah"]},{"emoji":"🚹","aliases":["mens"]},{"emoji":"🧜‍♀️","aliases":["mermaid"]},{"emoji":"🧜‍♂️","aliases":["merman"]},{"emoji":"🧜","aliases":["merperson"]},{"emoji":"🤘","aliases":["metal"]},{"emoji":"🚇","aliases":["metro"]},{"emoji":"🇲🇽","aliases":["mexico"]},{"emoji":"🦠","aliases":["microbe"]},{"emoji":"🇫🇲","aliases":["micronesia"]},{"emoji":"🎤","aliases":["microphone"]},{"emoji":"🔬","aliases":["microscope"]},{"emoji":"🖕","aliases":["middle_finger","fu"]},{"emoji":"🪖","aliases":["military_helmet"]},{"emoji":"🥛","aliases":["milk_glass"]},{"emoji":"🌌","aliases":["milky_way"]},{"emoji":"🚐","aliases":["minibus"]},{"emoji":"💽","aliases":["minidisc"]},{"emoji":"🪞","aliases":["mirror"]},{"emoji":"🪩","aliases":["mirror_ball"]},{"emoji":"📴","aliases":["mobile_phone_off"]},{"emoji":"🇲🇩","aliases":["moldova"]},{"emoji":"🇲🇨","aliases":["monaco"]},{"emoji":"🤑","aliases":["money_mouth_face"]},{"emoji":"💸","aliases":["money_with_wings"]},{"emoji":"💰","aliases":["moneybag"]},{"emoji":"🇲🇳","aliases":["mongolia"]},{"emoji":"🐒","aliases":["monkey"]},{"emoji":"🐵","aliases":["monkey_face"]},{"emoji":"🧐","aliases":["monocle_face"]},{"emoji":"🚝","aliases":["monorail"]},{"emoji":"🇲🇪","aliases":["montenegro"]},{"emoji":"🇲🇸","aliases":["montserrat"]},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"]},{"emoji":"🥮","aliases":["moon_cake"]},{"emoji":"🇲🇦","aliases":["morocco"]},{"emoji":"🎓","aliases":["mortar_board"]},{"emoji":"🕌","aliases":["mosque"]},{"emoji":"🦟","aliases":["mosquito"]},{"emoji":"🛥️","aliases":["motor_boat"]},{"emoji":"🛵","aliases":["motor_scooter"]},{"emoji":"🏍️","aliases":["motorcycle"]},{"emoji":"🦼","aliases":["motorized_wheelchair"]},{"emoji":"🛣️","aliases":["motorway"]},{"emoji":"🗻","aliases":["mount_fuji"]},{"emoji":"⛰️","aliases":["mountain"]},{"emoji":"🚵","aliases":["mountain_bicyclist"]},{"emoji":"🚵‍♂️","aliases":["mountain_biking_man"]},{"emoji":"🚵‍♀️","aliases":["mountain_biking_woman"]},{"emoji":"🚠","aliases":["mountain_cableway"]},{"emoji":"🚞","aliases":["mountain_railway"]},{"emoji":"🏔️","aliases":["mountain_snow"]},{"emoji":"🐭","aliases":["mouse"]},{"emoji":"🐁","aliases":["mouse2"]},{"emoji":"🪤","aliases":["mouse_trap"]},{"emoji":"🎥","aliases":["movie_camera"]},{"emoji":"🗿","aliases":["moyai"]},{"emoji":"🇲🇿","aliases":["mozambique"]},{"emoji":"🤶","aliases":["mrs_claus"]},{"emoji":"💪","aliases":["muscle"]},{"emoji":"🍄","aliases":["mushroom"]},{"emoji":"🎹","aliases":["musical_keyboard"]},{"emoji":"🎵","aliases":["musical_note"]},{"emoji":"🎼","aliases":["musical_score"]},{"emoji":"🔇","aliases":["mute"]},{"emoji":"🧑‍🎄","aliases":["mx_claus"]},{"emoji":"🇲🇲","aliases":["myanmar"]},{"emoji":"💅","aliases":["nail_care"]},{"emoji":"📛","aliases":["name_badge"]},{"emoji":"🇳🇦","aliases":["namibia"]},{"emoji":"🏞️","aliases":["national_park"]},{"emoji":"🇳🇷","aliases":["nauru"]},{"emoji":"🤢","aliases":["nauseated_face"]},{"emoji":"🧿","aliases":["nazar_amulet"]},{"emoji":"👔","aliases":["necktie"]},{"emoji":"❎","aliases":["negative_squared_cross_mark"]},{"emoji":"🇳🇵","aliases":["nepal"]},{"emoji":"🤓","aliases":["nerd_face"]},{"emoji":"🪺","aliases":["nest_with_eggs"]},{"emoji":"🪆","aliases":["nesting_dolls"]},{"emoji":"🇳🇱","aliases":["netherlands"]},{"emoji":"😐","aliases":["neutral_face"]},{"emoji":"🆕","aliases":["new"]},{"emoji":"🇳🇨","aliases":["new_caledonia"]},{"emoji":"🌑","aliases":["new_moon"]},{"emoji":"🌚","aliases":["new_moon_with_face"]},{"emoji":"🇳🇿","aliases":["new_zealand"]},{"emoji":"📰","aliases":["newspaper"]},{"emoji":"🗞️","aliases":["newspaper_roll"]},{"emoji":"⏭️","aliases":["next_track_button"]},{"emoji":"🆖","aliases":["ng"]},{"emoji":"🇳🇮","aliases":["nicaragua"]},{"emoji":"🇳🇪","aliases":["niger"]},{"emoji":"🇳🇬","aliases":["nigeria"]},{"emoji":"🌃","aliases":["night_with_stars"]},{"emoji":"9️⃣","aliases":["nine"]},{"emoji":"🥷","aliases":["ninja"]},{"emoji":"🇳🇺","aliases":["niue"]},{"emoji":"🔕","aliases":["no_bell"]},{"emoji":"🚳","aliases":["no_bicycles"]},{"emoji":"⛔","aliases":["no_entry"]},{"emoji":"🚫","aliases":["no_entry_sign"]},{"emoji":"🙅","aliases":["no_good"]},{"emoji":"🙅‍♂️","aliases":["no_good_man","ng_man"]},{"emoji":"🙅‍♀️","aliases":["no_good_woman","ng_woman"]},{"emoji":"📵","aliases":["no_mobile_phones"]},{"emoji":"😶","aliases":["no_mouth"]},{"emoji":"🚷","aliases":["no_pedestrians"]},{"emoji":"🚭","aliases":["no_smoking"]},{"emoji":"🚱","aliases":["non-potable_water"]},{"emoji":"🇳🇫","aliases":["norfolk_island"]},{"emoji":"🇰🇵","aliases":["north_korea"]},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"]},{"emoji":"🇳🇴","aliases":["norway"]},{"emoji":"👃","aliases":["nose"]},{"emoji":"📓","aliases":["notebook"]},{"emoji":"📔","aliases":["notebook_with_decorative_cover"]},{"emoji":"🎶","aliases":["notes"]},{"emoji":"🔩","aliases":["nut_and_bolt"]},{"emoji":"⭕","aliases":["o"]},{"emoji":"🅾️","aliases":["o2"]},{"emoji":"🌊","aliases":["ocean"]},{"emoji":"🐙","aliases":["octopus"]},{"emoji":"🍢","aliases":["oden"]},{"emoji":"🏢","aliases":["office"]},{"emoji":"🧑‍💼","aliases":["office_worker"]},{"emoji":"🛢️","aliases":["oil_drum"]},{"emoji":"🆗","aliases":["ok"]},{"emoji":"👌","aliases":["ok_hand"]},{"emoji":"🙆‍♂️","aliases":["ok_man"]},{"emoji":"🙆","aliases":["ok_person"]},{"emoji":"🙆‍♀️","aliases":["ok_woman"]},{"emoji":"🗝️","aliases":["old_key"]},{"emoji":"🧓","aliases":["older_adult"]},{"emoji":"👴","aliases":["older_man"]},{"emoji":"👵","aliases":["older_woman"]},{"emoji":"🫒","aliases":["olive"]},{"emoji":"🕉️","aliases":["om"]},{"emoji":"🇴🇲","aliases":["oman"]},{"emoji":"🔛","aliases":["on"]},{"emoji":"🚘","aliases":["oncoming_automobile"]},{"emoji":"🚍","aliases":["oncoming_bus"]},{"emoji":"🚔","aliases":["oncoming_police_car"]},{"emoji":"🚖","aliases":["oncoming_taxi"]},{"emoji":"1️⃣","aliases":["one"]},{"emoji":"🩱","aliases":["one_piece_swimsuit"]},{"emoji":"🧅","aliases":["onion"]},{"emoji":"📂","aliases":["open_file_folder"]},{"emoji":"👐","aliases":["open_hands"]},{"emoji":"😮","aliases":["open_mouth"]},{"emoji":"☂️","aliases":["open_umbrella"]},{"emoji":"⛎","aliases":["ophiuchus"]},{"emoji":"📙","aliases":["orange_book"]},{"emoji":"🟠","aliases":["orange_circle"]},{"emoji":"🧡","aliases":["orange_heart"]},{"emoji":"🟧","aliases":["orange_square"]},{"emoji":"🦧","aliases":["orangutan"]},{"emoji":"☦️","aliases":["orthodox_cross"]},{"emoji":"🦦","aliases":["otter"]},{"emoji":"📤","aliases":["outbox_tray"]},{"emoji":"🦉","aliases":["owl"]},{"emoji":"🐂","aliases":["ox"]},{"emoji":"🦪","aliases":["oyster"]},{"emoji":"📦","aliases":["package"]},{"emoji":"📄","aliases":["page_facing_up"]},{"emoji":"📃","aliases":["page_with_curl"]},{"emoji":"📟","aliases":["pager"]},{"emoji":"🖌️","aliases":["paintbrush"]},{"emoji":"🇵🇰","aliases":["pakistan"]},{"emoji":"🇵🇼","aliases":["palau"]},{"emoji":"🇵🇸","aliases":["palestinian_territories"]},{"emoji":"🫳","aliases":["palm_down_hand"]},{"emoji":"🌴","aliases":["palm_tree"]},{"emoji":"🫴","aliases":["palm_up_hand"]},{"emoji":"🤲","aliases":["palms_up_together"]},{"emoji":"🇵🇦","aliases":["panama"]},{"emoji":"🥞","aliases":["pancakes"]},{"emoji":"🐼","aliases":["panda_face"]},{"emoji":"📎","aliases":["paperclip"]},{"emoji":"🖇️","aliases":["paperclips"]},{"emoji":"🇵🇬","aliases":["papua_new_guinea"]},{"emoji":"🪂","aliases":["parachute"]},{"emoji":"🇵🇾","aliases":["paraguay"]},{"emoji":"⛱️","aliases":["parasol_on_ground"]},{"emoji":"🅿️","aliases":["parking"]},{"emoji":"🦜","aliases":["parrot"]},{"emoji":"〽️","aliases":["part_alternation_mark"]},{"emoji":"⛅","aliases":["partly_sunny"]},{"emoji":"🥳","aliases":["partying_face"]},{"emoji":"🛳️","aliases":["passenger_ship"]},{"emoji":"🛂","aliases":["passport_control"]},{"emoji":"⏸️","aliases":["pause_button"]},{"emoji":"☮️","aliases":["peace_symbol"]},{"emoji":"🍑","aliases":["peach"]},{"emoji":"🦚","aliases":["peacock"]},{"emoji":"🥜","aliases":["peanuts"]},{"emoji":"🍐","aliases":["pear"]},{"emoji":"🖊️","aliases":["pen"]},{"emoji":"✏️","aliases":["pencil2"]},{"emoji":"🐧","aliases":["penguin"]},{"emoji":"😔","aliases":["pensive"]},{"emoji":"🧑‍🤝‍🧑","aliases":["people_holding_hands"]},{"emoji":"🫂","aliases":["people_hugging"]},{"emoji":"🎭","aliases":["performing_arts"]},{"emoji":"😣","aliases":["persevere"]},{"emoji":"🧑‍🦲","aliases":["person_bald"]},{"emoji":"🧑‍🦱","aliases":["person_curly_hair"]},{"emoji":"🧑‍🍼","aliases":["person_feeding_baby"]},{"emoji":"🤺","aliases":["person_fencing"]},{"emoji":"🧑‍🦽","aliases":["person_in_manual_wheelchair"]},{"emoji":"🧑‍🦼","aliases":["person_in_motorized_wheelchair"]},{"emoji":"🤵","aliases":["person_in_tuxedo"]},{"emoji":"🧑‍🦰","aliases":["person_red_hair"]},{"emoji":"🧑‍🦳","aliases":["person_white_hair"]},{"emoji":"🫅","aliases":["person_with_crown"]},{"emoji":"🧑‍🦯","aliases":["person_with_probing_cane"]},{"emoji":"👳","aliases":["person_with_turban"]},{"emoji":"👰","aliases":["person_with_veil"]},{"emoji":"🇵🇪","aliases":["peru"]},{"emoji":"🧫","aliases":["petri_dish"]},{"emoji":"🇵🇭","aliases":["philippines"]},{"emoji":"☎️","aliases":["phone","telephone"]},{"emoji":"⛏️","aliases":["pick"]},{"emoji":"🛻","aliases":["pickup_truck"]},{"emoji":"🥧","aliases":["pie"]},{"emoji":"🐷","aliases":["pig"]},{"emoji":"🐖","aliases":["pig2"]},{"emoji":"🐽","aliases":["pig_nose"]},{"emoji":"💊","aliases":["pill"]},{"emoji":"🧑‍✈️","aliases":["pilot"]},{"emoji":"🪅","aliases":["pinata"]},{"emoji":"🤌","aliases":["pinched_fingers"]},{"emoji":"🤏","aliases":["pinching_hand"]},{"emoji":"🍍","aliases":["pineapple"]},{"emoji":"🏓","aliases":["ping_pong"]},{"emoji":"🏴‍☠️","aliases":["pirate_flag"]},{"emoji":"♓","aliases":["pisces"]},{"emoji":"🇵🇳","aliases":["pitcairn_islands"]},{"emoji":"🍕","aliases":["pizza"]},{"emoji":"🪧","aliases":["placard"]},{"emoji":"🛐","aliases":["place_of_worship"]},{"emoji":"🍽️","aliases":["plate_with_cutlery"]},{"emoji":"⏯️","aliases":["play_or_pause_button"]},{"emoji":"🛝","aliases":["playground_slide"]},{"emoji":"🥺","aliases":["pleading_face"]},{"emoji":"🪠","aliases":["plunger"]},{"emoji":"👇","aliases":["point_down"]},{"emoji":"👈","aliases":["point_left"]},{"emoji":"👉","aliases":["point_right"]},{"emoji":"☝️","aliases":["point_up"]},{"emoji":"👆","aliases":["point_up_2"]},{"emoji":"🇵🇱","aliases":["poland"]},{"emoji":"🐻‍❄️","aliases":["polar_bear"]},{"emoji":"🚓","aliases":["police_car"]},{"emoji":"👮","aliases":["police_officer","cop"]},{"emoji":"👮‍♂️","aliases":["policeman"]},{"emoji":"👮‍♀️","aliases":["policewoman"]},{"emoji":"🐩","aliases":["poodle"]},{"emoji":"🍿","aliases":["popcorn"]},{"emoji":"🇵🇹","aliases":["portugal"]},{"emoji":"🏣","aliases":["post_office"]},{"emoji":"📯","aliases":["postal_horn"]},{"emoji":"📮","aliases":["postbox"]},{"emoji":"🚰","aliases":["potable_water"]},{"emoji":"🥔","aliases":["potato"]},{"emoji":"🪴","aliases":["potted_plant"]},{"emoji":"👝","aliases":["pouch"]},{"emoji":"🍗","aliases":["poultry_leg"]},{"emoji":"💷","aliases":["pound"]},{"emoji":"🫗","aliases":["pouring_liquid"]},{"emoji":"😾","aliases":["pouting_cat"]},{"emoji":"🙎","aliases":["pouting_face"]},{"emoji":"🙎‍♂️","aliases":["pouting_man"]},{"emoji":"🙎‍♀️","aliases":["pouting_woman"]},{"emoji":"🙏","aliases":["pray"]},{"emoji":"📿","aliases":["prayer_beads"]},{"emoji":"🫃","aliases":["pregnant_man"]},{"emoji":"🫄","aliases":["pregnant_person"]},{"emoji":"🤰","aliases":["pregnant_woman"]},{"emoji":"🥨","aliases":["pretzel"]},{"emoji":"⏮️","aliases":["previous_track_button"]},{"emoji":"🤴","aliases":["prince"]},{"emoji":"👸","aliases":["princess"]},{"emoji":"🖨️","aliases":["printer"]},{"emoji":"🦯","aliases":["probing_cane"]},{"emoji":"🇵🇷","aliases":["puerto_rico"]},{"emoji":"🟣","aliases":["purple_circle"]},{"emoji":"💜","aliases":["purple_heart"]},{"emoji":"🟪","aliases":["purple_square"]},{"emoji":"👛","aliases":["purse"]},{"emoji":"📌","aliases":["pushpin"]},{"emoji":"🚮","aliases":["put_litter_in_its_place"]},{"emoji":"🇶🇦","aliases":["qatar"]},{"emoji":"❓","aliases":["question"]},{"emoji":"🐰","aliases":["rabbit"]},{"emoji":"🐇","aliases":["rabbit2"]},{"emoji":"🦝","aliases":["raccoon"]},{"emoji":"🐎","aliases":["racehorse"]},{"emoji":"🏎️","aliases":["racing_car"]},{"emoji":"📻","aliases":["radio"]},{"emoji":"🔘","aliases":["radio_button"]},{"emoji":"☢️","aliases":["radioactive"]},{"emoji":"😡","aliases":["rage","pout"]},{"emoji":"🚃","aliases":["railway_car"]},{"emoji":"🛤️","aliases":["railway_track"]},{"emoji":"🌈","aliases":["rainbow"]},{"emoji":"🏳️‍🌈","aliases":["rainbow_flag"]},{"emoji":"🤚","aliases":["raised_back_of_hand"]},{"emoji":"🤨","aliases":["raised_eyebrow"]},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"]},{"emoji":"🙌","aliases":["raised_hands"]},{"emoji":"🙋","aliases":["raising_hand"]},{"emoji":"🙋‍♂️","aliases":["raising_hand_man"]},{"emoji":"🙋‍♀️","aliases":["raising_hand_woman"]},{"emoji":"🐏","aliases":["ram"]},{"emoji":"🍜","aliases":["ramen"]},{"emoji":"🐀","aliases":["rat"]},{"emoji":"🪒","aliases":["razor"]},{"emoji":"🧾","aliases":["receipt"]},{"emoji":"⏺️","aliases":["record_button"]},{"emoji":"♻️","aliases":["recycle"]},{"emoji":"🔴","aliases":["red_circle"]},{"emoji":"🧧","aliases":["red_envelope"]},{"emoji":"👨‍🦰","aliases":["red_haired_man"]},{"emoji":"👩‍🦰","aliases":["red_haired_woman"]},{"emoji":"🟥","aliases":["red_square"]},{"emoji":"®️","aliases":["registered"]},{"emoji":"☺️","aliases":["relaxed"]},{"emoji":"😌","aliases":["relieved"]},{"emoji":"🎗️","aliases":["reminder_ribbon"]},{"emoji":"🔁","aliases":["repeat"]},{"emoji":"🔂","aliases":["repeat_one"]},{"emoji":"⛑️","aliases":["rescue_worker_helmet"]},{"emoji":"🚻","aliases":["restroom"]},{"emoji":"🇷🇪","aliases":["reunion"]},{"emoji":"💞","aliases":["revolving_hearts"]},{"emoji":"⏪","aliases":["rewind"]},{"emoji":"🦏","aliases":["rhinoceros"]},{"emoji":"🎀","aliases":["ribbon"]},{"emoji":"🍚","aliases":["rice"]},{"emoji":"🍙","aliases":["rice_ball"]},{"emoji":"🍘","aliases":["rice_cracker"]},{"emoji":"🎑","aliases":["rice_scene"]},{"emoji":"🗯️","aliases":["right_anger_bubble"]},{"emoji":"🫱","aliases":["rightwards_hand"]},{"emoji":"💍","aliases":["ring"]},{"emoji":"🛟","aliases":["ring_buoy"]},{"emoji":"🪐","aliases":["ringed_planet"]},{"emoji":"🤖","aliases":["robot"]},{"emoji":"🪨","aliases":["rock"]},{"emoji":"🚀","aliases":["rocket"]},{"emoji":"🤣","aliases":["rofl"]},{"emoji":"🙄","aliases":["roll_eyes"]},{"emoji":"🧻","aliases":["roll_of_paper"]},{"emoji":"🎢","aliases":["roller_coaster"]},{"emoji":"🛼","aliases":["roller_skate"]},{"emoji":"🇷🇴","aliases":["romania"]},{"emoji":"🐓","aliases":["rooster"]},{"emoji":"🌹","aliases":["rose"]},{"emoji":"🏵️","aliases":["rosette"]},{"emoji":"🚨","aliases":["rotating_light"]},{"emoji":"📍","aliases":["round_pushpin"]},{"emoji":"🚣","aliases":["rowboat"]},{"emoji":"🚣‍♂️","aliases":["rowing_man"]},{"emoji":"🚣‍♀️","aliases":["rowing_woman"]},{"emoji":"🇷🇺","aliases":["ru"]},{"emoji":"🏉","aliases":["rugby_football"]},{"emoji":"🏃","aliases":["runner","running"]},{"emoji":"🏃‍♂️","aliases":["running_man"]},{"emoji":"🎽","aliases":["running_shirt_with_sash"]},{"emoji":"🏃‍♀️","aliases":["running_woman"]},{"emoji":"🇷🇼","aliases":["rwanda"]},{"emoji":"🈂️","aliases":["sa"]},{"emoji":"🧷","aliases":["safety_pin"]},{"emoji":"🦺","aliases":["safety_vest"]},{"emoji":"♐","aliases":["sagittarius"]},{"emoji":"🍶","aliases":["sake"]},{"emoji":"🧂","aliases":["salt"]},{"emoji":"🫡","aliases":["saluting_face"]},{"emoji":"🇼🇸","aliases":["samoa"]},{"emoji":"🇸🇲","aliases":["san_marino"]},{"emoji":"👡","aliases":["sandal"]},{"emoji":"🥪","aliases":["sandwich"]},{"emoji":"🎅","aliases":["santa"]},{"emoji":"🇸🇹","aliases":["sao_tome_principe"]},{"emoji":"🥻","aliases":["sari"]},{"emoji":"📡","aliases":["satellite"]},{"emoji":"🇸🇦","aliases":["saudi_arabia"]},{"emoji":"🧖‍♂️","aliases":["sauna_man"]},{"emoji":"🧖","aliases":["sauna_person"]},{"emoji":"🧖‍♀️","aliases":["sauna_woman"]},{"emoji":"🦕","aliases":["sauropod"]},{"emoji":"🎷","aliases":["saxophone"]},{"emoji":"🧣","aliases":["scarf"]},{"emoji":"🏫","aliases":["school"]},{"emoji":"🎒","aliases":["school_satchel"]},{"emoji":"🧑‍🔬","aliases":["scientist"]},{"emoji":"✂️","aliases":["scissors"]},{"emoji":"🦂","aliases":["scorpion"]},{"emoji":"♏","aliases":["scorpius"]},{"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","aliases":["scotland"]},{"emoji":"😱","aliases":["scream"]},{"emoji":"🙀","aliases":["scream_cat"]},{"emoji":"🪛","aliases":["screwdriver"]},{"emoji":"📜","aliases":["scroll"]},{"emoji":"🦭","aliases":["seal"]},{"emoji":"💺","aliases":["seat"]},{"emoji":"㊙️","aliases":["secret"]},{"emoji":"🙈","aliases":["see_no_evil"]},{"emoji":"🌱","aliases":["seedling"]},{"emoji":"🤳","aliases":["selfie"]},{"emoji":"🇸🇳","aliases":["senegal"]},{"emoji":"🇷🇸","aliases":["serbia"]},{"emoji":"🐕‍🦺","aliases":["service_dog"]},{"emoji":"7️⃣","aliases":["seven"]},{"emoji":"🪡","aliases":["sewing_needle"]},{"emoji":"🇸🇨","aliases":["seychelles"]},{"emoji":"🥘","aliases":["shallow_pan_of_food"]},{"emoji":"☘️","aliases":["shamrock"]},{"emoji":"🦈","aliases":["shark"]},{"emoji":"🍧","aliases":["shaved_ice"]},{"emoji":"🐑","aliases":["sheep"]},{"emoji":"🐚","aliases":["shell"]},{"emoji":"🛡️","aliases":["shield"]},{"emoji":"⛩️","aliases":["shinto_shrine"]},{"emoji":"🚢","aliases":["ship"]},{"emoji":"👕","aliases":["shirt","tshirt"]},{"emoji":"🛍️","aliases":["shopping"]},{"emoji":"🛒","aliases":["shopping_cart"]},{"emoji":"🩳","aliases":["shorts"]},{"emoji":"🚿","aliases":["shower"]},{"emoji":"🦐","aliases":["shrimp"]},{"emoji":"🤷","aliases":["shrug"]},{"emoji":"🤫","aliases":["shushing_face"]},{"emoji":"🇸🇱","aliases":["sierra_leone"]},{"emoji":"📶","aliases":["signal_strength"]},{"emoji":"🇸🇬","aliases":["singapore"]},{"emoji":"🧑‍🎤","aliases":["singer"]},{"emoji":"🇸🇽","aliases":["sint_maarten"]},{"emoji":"6️⃣","aliases":["six"]},{"emoji":"🔯","aliases":["six_pointed_star"]},{"emoji":"🛹","aliases":["skateboard"]},{"emoji":"🎿","aliases":["ski"]},{"emoji":"⛷️","aliases":["skier"]},{"emoji":"💀","aliases":["skull"]},{"emoji":"☠️","aliases":["skull_and_crossbones"]},{"emoji":"🦨","aliases":["skunk"]},{"emoji":"🛷","aliases":["sled"]},{"emoji":"😴","aliases":["sleeping"]},{"emoji":"🛌","aliases":["sleeping_bed"]},{"emoji":"😪","aliases":["sleepy"]},{"emoji":"🙁","aliases":["slightly_frowning_face"]},{"emoji":"🙂","aliases":["slightly_smiling_face"]},{"emoji":"🎰","aliases":["slot_machine"]},{"emoji":"🦥","aliases":["sloth"]},{"emoji":"🇸🇰","aliases":["slovakia"]},{"emoji":"🇸🇮","aliases":["slovenia"]},{"emoji":"🛩️","aliases":["small_airplane"]},{"emoji":"🔹","aliases":["small_blue_diamond"]},{"emoji":"🔸","aliases":["small_orange_diamond"]},{"emoji":"🔺","aliases":["small_red_triangle"]},{"emoji":"🔻","aliases":["small_red_triangle_down"]},{"emoji":"😄","aliases":["smile"]},{"emoji":"😸","aliases":["smile_cat"]},{"emoji":"😃","aliases":["smiley"]},{"emoji":"😺","aliases":["smiley_cat"]},{"emoji":"🥲","aliases":["smiling_face_with_tear"]},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"]},{"emoji":"😈","aliases":["smiling_imp"]},{"emoji":"😏","aliases":["smirk"]},{"emoji":"😼","aliases":["smirk_cat"]},{"emoji":"🚬","aliases":["smoking"]},{"emoji":"🐌","aliases":["snail"]},{"emoji":"🐍","aliases":["snake"]},{"emoji":"🤧","aliases":["sneezing_face"]},{"emoji":"🏂","aliases":["snowboarder"]},{"emoji":"❄️","aliases":["snowflake"]},{"emoji":"⛄","aliases":["snowman"]},{"emoji":"☃️","aliases":["snowman_with_snow"]},{"emoji":"🧼","aliases":["soap"]},{"emoji":"😭","aliases":["sob"]},{"emoji":"⚽","aliases":["soccer"]},{"emoji":"🧦","aliases":["socks"]},{"emoji":"🥎","aliases":["softball"]},{"emoji":"🇸🇧","aliases":["solomon_islands"]},{"emoji":"🇸🇴","aliases":["somalia"]},{"emoji":"🔜","aliases":["soon"]},{"emoji":"🆘","aliases":["sos"]},{"emoji":"🔉","aliases":["sound"]},{"emoji":"🇿🇦","aliases":["south_africa"]},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"]},{"emoji":"🇸🇸","aliases":["south_sudan"]},{"emoji":"👾","aliases":["space_invader"]},{"emoji":"♠️","aliases":["spades"]},{"emoji":"🍝","aliases":["spaghetti"]},{"emoji":"❇️","aliases":["sparkle"]},{"emoji":"🎇","aliases":["sparkler"]},{"emoji":"✨","aliases":["sparkles"]},{"emoji":"💖","aliases":["sparkling_heart"]},{"emoji":"🙊","aliases":["speak_no_evil"]},{"emoji":"🔈","aliases":["speaker"]},{"emoji":"🗣️","aliases":["speaking_head"]},{"emoji":"💬","aliases":["speech_balloon"]},{"emoji":"🚤","aliases":["speedboat"]},{"emoji":"🕷️","aliases":["spider"]},{"emoji":"🕸️","aliases":["spider_web"]},{"emoji":"🗓️","aliases":["spiral_calendar"]},{"emoji":"🗒️","aliases":["spiral_notepad"]},{"emoji":"🧽","aliases":["sponge"]},{"emoji":"🥄","aliases":["spoon"]},{"emoji":"🦑","aliases":["squid"]},{"emoji":"🇱🇰","aliases":["sri_lanka"]},{"emoji":"🇧🇱","aliases":["st_barthelemy"]},{"emoji":"🇸🇭","aliases":["st_helena"]},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"]},{"emoji":"🇱🇨","aliases":["st_lucia"]},{"emoji":"🇲🇫","aliases":["st_martin"]},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"]},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"]},{"emoji":"🏟️","aliases":["stadium"]},{"emoji":"🧍‍♂️","aliases":["standing_man"]},{"emoji":"🧍","aliases":["standing_person"]},{"emoji":"🧍‍♀️","aliases":["standing_woman"]},{"emoji":"⭐","aliases":["star"]},{"emoji":"🌟","aliases":["star2"]},{"emoji":"☪️","aliases":["star_and_crescent"]},{"emoji":"✡️","aliases":["star_of_david"]},{"emoji":"🤩","aliases":["star_struck"]},{"emoji":"🌠","aliases":["stars"]},{"emoji":"🚉","aliases":["station"]},{"emoji":"🗽","aliases":["statue_of_liberty"]},{"emoji":"🚂","aliases":["steam_locomotive"]},{"emoji":"🩺","aliases":["stethoscope"]},{"emoji":"🍲","aliases":["stew"]},{"emoji":"⏹️","aliases":["stop_button"]},{"emoji":"🛑","aliases":["stop_sign"]},{"emoji":"⏱️","aliases":["stopwatch"]},{"emoji":"📏","aliases":["straight_ruler"]},{"emoji":"🍓","aliases":["strawberry"]},{"emoji":"😛","aliases":["stuck_out_tongue"]},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"]},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"]},{"emoji":"🧑‍🎓","aliases":["student"]},{"emoji":"🎙️","aliases":["studio_microphone"]},{"emoji":"🥙","aliases":["stuffed_flatbread"]},{"emoji":"🇸🇩","aliases":["sudan"]},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"]},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"]},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"]},{"emoji":"🌞","aliases":["sun_with_face"]},{"emoji":"🌻","aliases":["sunflower"]},{"emoji":"😎","aliases":["sunglasses"]},{"emoji":"☀️","aliases":["sunny"]},{"emoji":"🌅","aliases":["sunrise"]},{"emoji":"🌄","aliases":["sunrise_over_mountains"]},{"emoji":"🦸","aliases":["superhero"]},{"emoji":"🦸‍♂️","aliases":["superhero_man"]},{"emoji":"🦸‍♀️","aliases":["superhero_woman"]},{"emoji":"🦹","aliases":["supervillain"]},{"emoji":"🦹‍♂️","aliases":["supervillain_man"]},{"emoji":"🦹‍♀️","aliases":["supervillain_woman"]},{"emoji":"🏄","aliases":["surfer"]},{"emoji":"🏄‍♂️","aliases":["surfing_man"]},{"emoji":"🏄‍♀️","aliases":["surfing_woman"]},{"emoji":"🇸🇷","aliases":["suriname"]},{"emoji":"🍣","aliases":["sushi"]},{"emoji":"🚟","aliases":["suspension_railway"]},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"]},{"emoji":"🦢","aliases":["swan"]},{"emoji":"🇸🇿","aliases":["swaziland"]},{"emoji":"😓","aliases":["sweat"]},{"emoji":"💦","aliases":["sweat_drops"]},{"emoji":"😅","aliases":["sweat_smile"]},{"emoji":"🇸🇪","aliases":["sweden"]},{"emoji":"🍠","aliases":["sweet_potato"]},{"emoji":"🩲","aliases":["swim_brief"]},{"emoji":"🏊","aliases":["swimmer"]},{"emoji":"🏊‍♂️","aliases":["swimming_man"]},{"emoji":"🏊‍♀️","aliases":["swimming_woman"]},{"emoji":"🇨🇭","aliases":["switzerland"]},{"emoji":"🔣","aliases":["symbols"]},{"emoji":"🕍","aliases":["synagogue"]},{"emoji":"🇸🇾","aliases":["syria"]},{"emoji":"💉","aliases":["syringe"]},{"emoji":"🦖","aliases":["t-rex"]},{"emoji":"🌮","aliases":["taco"]},{"emoji":"🎉","aliases":["tada","hooray"]},{"emoji":"🇹🇼","aliases":["taiwan"]},{"emoji":"🇹🇯","aliases":["tajikistan"]},{"emoji":"🥡","aliases":["takeout_box"]},{"emoji":"🫔","aliases":["tamale"]},{"emoji":"🎋","aliases":["tanabata_tree"]},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"]},{"emoji":"🇹🇿","aliases":["tanzania"]},{"emoji":"♉","aliases":["taurus"]},{"emoji":"🚕","aliases":["taxi"]},{"emoji":"🍵","aliases":["tea"]},{"emoji":"🧑‍🏫","aliases":["teacher"]},{"emoji":"🫖","aliases":["teapot"]},{"emoji":"🧑‍💻","aliases":["technologist"]},{"emoji":"🧸","aliases":["teddy_bear"]},{"emoji":"📞","aliases":["telephone_receiver"]},{"emoji":"🔭","aliases":["telescope"]},{"emoji":"🎾","aliases":["tennis"]},{"emoji":"⛺","aliases":["tent"]},{"emoji":"🧪","aliases":["test_tube"]},{"emoji":"🇹🇭","aliases":["thailand"]},{"emoji":"🌡️","aliases":["thermometer"]},{"emoji":"🤔","aliases":["thinking"]},{"emoji":"🩴","aliases":["thong_sandal"]},{"emoji":"💭","aliases":["thought_balloon"]},{"emoji":"🧵","aliases":["thread"]},{"emoji":"3️⃣","aliases":["three"]},{"emoji":"🎫","aliases":["ticket"]},{"emoji":"🎟️","aliases":["tickets"]},{"emoji":"🐯","aliases":["tiger"]},{"emoji":"🐅","aliases":["tiger2"]},{"emoji":"⏲️","aliases":["timer_clock"]},{"emoji":"🇹🇱","aliases":["timor_leste"]},{"emoji":"💁‍♂️","aliases":["tipping_hand_man","sassy_man"]},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"]},{"emoji":"💁‍♀️","aliases":["tipping_hand_woman","sassy_woman"]},{"emoji":"😫","aliases":["tired_face"]},{"emoji":"™️","aliases":["tm"]},{"emoji":"🇹🇬","aliases":["togo"]},{"emoji":"🚽","aliases":["toilet"]},{"emoji":"🇹🇰","aliases":["tokelau"]},{"emoji":"🗼","aliases":["tokyo_tower"]},{"emoji":"🍅","aliases":["tomato"]},{"emoji":"🇹🇴","aliases":["tonga"]},{"emoji":"👅","aliases":["tongue"]},{"emoji":"🧰","aliases":["toolbox"]},{"emoji":"🦷","aliases":["tooth"]},{"emoji":"🪥","aliases":["toothbrush"]},{"emoji":"🔝","aliases":["top"]},{"emoji":"🎩","aliases":["tophat"]},{"emoji":"🌪️","aliases":["tornado"]},{"emoji":"🇹🇷","aliases":["tr"]},{"emoji":"🖲️","aliases":["trackball"]},{"emoji":"🚜","aliases":["tractor"]},{"emoji":"🚥","aliases":["traffic_light"]},{"emoji":"🚋","aliases":["train"]},{"emoji":"🚆","aliases":["train2"]},{"emoji":"🚊","aliases":["tram"]},{"emoji":"🏳️‍⚧️","aliases":["transgender_flag"]},{"emoji":"⚧️","aliases":["transgender_symbol"]},{"emoji":"🚩","aliases":["triangular_flag_on_post"]},{"emoji":"📐","aliases":["triangular_ruler"]},{"emoji":"🔱","aliases":["trident"]},{"emoji":"🇹🇹","aliases":["trinidad_tobago"]},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"]},{"emoji":"😤","aliases":["triumph"]},{"emoji":"🧌","aliases":["troll"]},{"emoji":"🚎","aliases":["trolleybus"]},{"emoji":"🏆","aliases":["trophy"]},{"emoji":"🍹","aliases":["tropical_drink"]},{"emoji":"🐠","aliases":["tropical_fish"]},{"emoji":"🚚","aliases":["truck"]},{"emoji":"🎺","aliases":["trumpet"]},{"emoji":"🌷","aliases":["tulip"]},{"emoji":"🥃","aliases":["tumbler_glass"]},{"emoji":"🇹🇳","aliases":["tunisia"]},{"emoji":"🦃","aliases":["turkey"]},{"emoji":"🇹🇲","aliases":["turkmenistan"]},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"]},{"emoji":"🐢","aliases":["turtle"]},{"emoji":"🇹🇻","aliases":["tuvalu"]},{"emoji":"📺","aliases":["tv"]},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"]},{"emoji":"2️⃣","aliases":["two"]},{"emoji":"💕","aliases":["two_hearts"]},{"emoji":"👬","aliases":["two_men_holding_hands"]},{"emoji":"👭","aliases":["two_women_holding_hands"]},{"emoji":"🈹","aliases":["u5272"]},{"emoji":"🈴","aliases":["u5408"]},{"emoji":"🈺","aliases":["u55b6"]},{"emoji":"🈯","aliases":["u6307"]},{"emoji":"🈷️","aliases":["u6708"]},{"emoji":"🈶","aliases":["u6709"]},{"emoji":"🈵","aliases":["u6e80"]},{"emoji":"🈚","aliases":["u7121"]},{"emoji":"🈸","aliases":["u7533"]},{"emoji":"🈲","aliases":["u7981"]},{"emoji":"🈳","aliases":["u7a7a"]},{"emoji":"🇺🇬","aliases":["uganda"]},{"emoji":"🇺🇦","aliases":["ukraine"]},{"emoji":"☔","aliases":["umbrella"]},{"emoji":"😒","aliases":["unamused"]},{"emoji":"🔞","aliases":["underage"]},{"emoji":"🦄","aliases":["unicorn"]},{"emoji":"🇦🇪","aliases":["united_arab_emirates"]},{"emoji":"🇺🇳","aliases":["united_nations"]},{"emoji":"🔓","aliases":["unlock"]},{"emoji":"🆙","aliases":["up"]},{"emoji":"🙃","aliases":["upside_down_face"]},{"emoji":"🇺🇾","aliases":["uruguay"]},{"emoji":"🇺🇸","aliases":["us"]},{"emoji":"🇺🇲","aliases":["us_outlying_islands"]},{"emoji":"🇻🇮","aliases":["us_virgin_islands"]},{"emoji":"🇺🇿","aliases":["uzbekistan"]},{"emoji":"✌️","aliases":["v"]},{"emoji":"🧛","aliases":["vampire"]},{"emoji":"🧛‍♂️","aliases":["vampire_man"]},{"emoji":"🧛‍♀️","aliases":["vampire_woman"]},{"emoji":"🇻🇺","aliases":["vanuatu"]},{"emoji":"🇻🇦","aliases":["vatican_city"]},{"emoji":"🇻🇪","aliases":["venezuela"]},{"emoji":"🚦","aliases":["vertical_traffic_light"]},{"emoji":"📼","aliases":["vhs"]},{"emoji":"📳","aliases":["vibration_mode"]},{"emoji":"📹","aliases":["video_camera"]},{"emoji":"🎮","aliases":["video_game"]},{"emoji":"🇻🇳","aliases":["vietnam"]},{"emoji":"🎻","aliases":["violin"]},{"emoji":"♍","aliases":["virgo"]},{"emoji":"🌋","aliases":["volcano"]},{"emoji":"🏐","aliases":["volleyball"]},{"emoji":"🤮","aliases":["vomiting_face"]},{"emoji":"🆚","aliases":["vs"]},{"emoji":"🖖","aliases":["vulcan_salute"]},{"emoji":"🧇","aliases":["waffle"]},{"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","aliases":["wales"]},{"emoji":"🚶","aliases":["walking"]},{"emoji":"🚶‍♂️","aliases":["walking_man"]},{"emoji":"🚶‍♀️","aliases":["walking_woman"]},{"emoji":"🇼🇫","aliases":["wallis_futuna"]},{"emoji":"🌘","aliases":["waning_crescent_moon"]},{"emoji":"🌖","aliases":["waning_gibbous_moon"]},{"emoji":"⚠️","aliases":["warning"]},{"emoji":"🗑️","aliases":["wastebasket"]},{"emoji":"⌚","aliases":["watch"]},{"emoji":"🐃","aliases":["water_buffalo"]},{"emoji":"🤽","aliases":["water_polo"]},{"emoji":"🍉","aliases":["watermelon"]},{"emoji":"👋","aliases":["wave"]},{"emoji":"〰️","aliases":["wavy_dash"]},{"emoji":"🌒","aliases":["waxing_crescent_moon"]},{"emoji":"🚾","aliases":["wc"]},{"emoji":"😩","aliases":["weary"]},{"emoji":"💒","aliases":["wedding"]},{"emoji":"🏋️","aliases":["weight_lifting"]},{"emoji":"🏋️‍♂️","aliases":["weight_lifting_man"]},{"emoji":"🏋️‍♀️","aliases":["weight_lifting_woman"]},{"emoji":"🇪🇭","aliases":["western_sahara"]},{"emoji":"🐳","aliases":["whale"]},{"emoji":"🐋","aliases":["whale2"]},{"emoji":"🛞","aliases":["wheel"]},{"emoji":"☸️","aliases":["wheel_of_dharma"]},{"emoji":"♿","aliases":["wheelchair"]},{"emoji":"✅","aliases":["white_check_mark"]},{"emoji":"⚪","aliases":["white_circle"]},{"emoji":"🏳️","aliases":["white_flag"]},{"emoji":"💮","aliases":["white_flower"]},{"emoji":"👨‍🦳","aliases":["white_haired_man"]},{"emoji":"👩‍🦳","aliases":["white_haired_woman"]},{"emoji":"🤍","aliases":["white_heart"]},{"emoji":"⬜","aliases":["white_large_square"]},{"emoji":"◽","aliases":["white_medium_small_square"]},{"emoji":"◻️","aliases":["white_medium_square"]},{"emoji":"▫️","aliases":["white_small_square"]},{"emoji":"🔳","aliases":["white_square_button"]},{"emoji":"🥀","aliases":["wilted_flower"]},{"emoji":"🎐","aliases":["wind_chime"]},{"emoji":"🌬️","aliases":["wind_face"]},{"emoji":"🪟","aliases":["window"]},{"emoji":"🍷","aliases":["wine_glass"]},{"emoji":"😉","aliases":["wink"]},{"emoji":"🐺","aliases":["wolf"]},{"emoji":"👩","aliases":["woman"]},{"emoji":"👩‍🎨","aliases":["woman_artist"]},{"emoji":"👩‍🚀","aliases":["woman_astronaut"]},{"emoji":"🧔‍♀️","aliases":["woman_beard"]},{"emoji":"🤸‍♀️","aliases":["woman_cartwheeling"]},{"emoji":"👩‍🍳","aliases":["woman_cook"]},{"emoji":"💃","aliases":["woman_dancing","dancer"]},{"emoji":"🤦‍♀️","aliases":["woman_facepalming"]},{"emoji":"👩‍🏭","aliases":["woman_factory_worker"]},{"emoji":"👩‍🌾","aliases":["woman_farmer"]},{"emoji":"👩‍🍼","aliases":["woman_feeding_baby"]},{"emoji":"👩‍🚒","aliases":["woman_firefighter"]},{"emoji":"👩‍⚕️","aliases":["woman_health_worker"]},{"emoji":"👩‍🦽","aliases":["woman_in_manual_wheelchair"]},{"emoji":"👩‍🦼","aliases":["woman_in_motorized_wheelchair"]},{"emoji":"🤵‍♀️","aliases":["woman_in_tuxedo"]},{"emoji":"👩‍⚖️","aliases":["woman_judge"]},{"emoji":"🤹‍♀️","aliases":["woman_juggling"]},{"emoji":"👩‍🔧","aliases":["woman_mechanic"]},{"emoji":"👩‍💼","aliases":["woman_office_worker"]},{"emoji":"👩‍✈️","aliases":["woman_pilot"]},{"emoji":"🤾‍♀️","aliases":["woman_playing_handball"]},{"emoji":"🤽‍♀️","aliases":["woman_playing_water_polo"]},{"emoji":"👩‍🔬","aliases":["woman_scientist"]},{"emoji":"🤷‍♀️","aliases":["woman_shrugging"]},{"emoji":"👩‍🎤","aliases":["woman_singer"]},{"emoji":"👩‍🎓","aliases":["woman_student"]},{"emoji":"👩‍🏫","aliases":["woman_teacher"]},{"emoji":"👩‍💻","aliases":["woman_technologist"]},{"emoji":"🧕","aliases":["woman_with_headscarf"]},{"emoji":"👩‍🦯","aliases":["woman_with_probing_cane"]},{"emoji":"👳‍♀️","aliases":["woman_with_turban"]},{"emoji":"👰‍♀️","aliases":["woman_with_veil","bride_with_veil"]},{"emoji":"👚","aliases":["womans_clothes"]},{"emoji":"👒","aliases":["womans_hat"]},{"emoji":"🤼‍♀️","aliases":["women_wrestling"]},{"emoji":"🚺","aliases":["womens"]},{"emoji":"🪵","aliases":["wood"]},{"emoji":"🥴","aliases":["woozy_face"]},{"emoji":"🗺️","aliases":["world_map"]},{"emoji":"🪱","aliases":["worm"]},{"emoji":"😟","aliases":["worried"]},{"emoji":"🔧","aliases":["wrench"]},{"emoji":"🤼","aliases":["wrestling"]},{"emoji":"✍️","aliases":["writing_hand"]},{"emoji":"❌","aliases":["x"]},{"emoji":"🩻","aliases":["x_ray"]},{"emoji":"🧶","aliases":["yarn"]},{"emoji":"🥱","aliases":["yawning_face"]},{"emoji":"🟡","aliases":["yellow_circle"]},{"emoji":"💛","aliases":["yellow_heart"]},{"emoji":"🟨","aliases":["yellow_square"]},{"emoji":"🇾🇪","aliases":["yemen"]},{"emoji":"💴","aliases":["yen"]},{"emoji":"☯️","aliases":["yin_yang"]},{"emoji":"🪀","aliases":["yo_yo"]},{"emoji":"😋","aliases":["yum"]},{"emoji":"🇿🇲","aliases":["zambia"]},{"emoji":"🤪","aliases":["zany_face"]},{"emoji":"⚡","aliases":["zap"]},{"emoji":"🦓","aliases":["zebra"]},{"emoji":"0️⃣","aliases":["zero"]},{"emoji":"🇿🇼","aliases":["zimbabwe"]},{"emoji":"🤐","aliases":["zipper_mouth_face"]},{"emoji":"🧟","aliases":["zombie"]},{"emoji":"🧟‍♂️","aliases":["zombie_man"]},{"emoji":"🧟‍♀️","aliases":["zombie_woman"]},{"emoji":"💤","aliases":["zzz"]}] \ No newline at end of file diff --git a/assets/favicon.svg b/assets/favicon.svg index 9df6b83b56..bcacdc0200 100644 --- a/assets/favicon.svg +++ b/assets/favicon.svg @@ -1,31 +1,27 @@ - - - - - - - - - - - + + + + + + + + + diff --git a/assets/go-licenses.json b/assets/go-licenses.json index db0b00865e..7f38756b84 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -679,6 +679,16 @@ "path": "github.com/olivere/elastic/v7/uritemplates/LICENSE", "licenseText": "Copyright (c) 2013 Joshua Tacoma\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, + { + "name": "github.com/opencontainers/go-digest", + "path": "github.com/opencontainers/go-digest/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n https://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n Copyright 2019, 2020 OCI Contributors\n Copyright 2016 Docker, Inc.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n https://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, + { + "name": "github.com/opencontainers/image-spec/specs-go", + "path": "github.com/opencontainers/image-spec/specs-go/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n Copyright 2016 The Linux Foundation.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "github.com/pierrec/lz4/v4", "path": "github.com/pierrec/lz4/v4/LICENSE", diff --git a/assets/logo.svg b/assets/logo.svg index 9df6b83b56..bcacdc0200 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,31 +1,27 @@ - - - - - - - - - - - + + + + + + + + + diff --git a/build/generate-emoji.go b/build/generate-emoji.go index 4ad6649b2e..5c8f2b653f 100644 --- a/build/generate-emoji.go +++ b/build/generate-emoji.go @@ -26,7 +26,7 @@ import ( const ( gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json" - maxUnicodeVersion = 12 + maxUnicodeVersion = 14 ) var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out") diff --git a/build/merge-forgejo-locales.go b/build/merge-forgejo-locales.go new file mode 100644 index 0000000000..4368fa9571 --- /dev/null +++ b/build/merge-forgejo-locales.go @@ -0,0 +1,92 @@ +// Copyright 2022 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "bufio" + "os" + "regexp" + "strings" + + "gopkg.in/ini.v1" +) + +const ( + trimPrefix = "gitea_" + sourceFolder = "options/locales/" +) + +// returns list of locales, still containing the file extension! +func generate_locale_list() []string { + localeFiles, _ := os.ReadDir(sourceFolder) + locales := []string{} + for _, localeFile := range localeFiles { + if !localeFile.IsDir() && strings.HasPrefix(localeFile.Name(), trimPrefix) { + locales = append(locales, strings.TrimPrefix(localeFile.Name(), trimPrefix)) + } + } + return locales +} + +// replace all occurrences of Gitea with Forgejo +func renameGiteaForgejo(filename string) []byte { + file, err := os.Open(filename) + if err != nil { + panic(err) + } + + replacer := strings.NewReplacer( + "Gitea", "Forgejo", + "https://docs.gitea.io/en-us/install-from-binary/", "https://forgejo.org/download/#installation-from-binary", + "https://github.com/go-gitea/gitea/tree/master/docker", "https://forgejo.org/download/#container-image", + "https://docs.gitea.io/en-us/install-from-package/", "https://forgejo.org/download", + "https://code.gitea.io/gitea", "https://forgejo.org/download", + "code.gitea.io/gitea", "Forgejo", + `GitHub`, `Codeberg`, + "https://github.com/go-gitea/gitea", "https://codeberg.org/forgejo/forgejo", + "https://blog.gitea.io", "https://forgejo.org/news", + ) + + out := make([]byte, 0, 1024) + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + out = append(out, []byte("\n"+line+"\n")...) + } else if strings.HasPrefix(line, "settings.web_hook_name_gitea") { + out = append(out, []byte("\n"+line+"\n")...) + out = append(out, []byte("settings.web_hook_name_forgejo = Forgejo\n")...) + } else if strings.HasPrefix(line, "migrate.gitea.description") { + re := regexp.MustCompile(`(.*Gitea)`) + out = append(out, []byte(re.ReplaceAllString(line, "${1}/Forgejo")+"\n")...) + } else { + out = append(out, []byte(replacer.Replace(line)+"\n")...) + } + } + file.Close() + return out +} + +func main() { + locales := generate_locale_list() + var err error + var localeFile *ini.File + for _, locale := range locales { + giteaLocale := sourceFolder + "gitea_" + locale + localeFile, err = ini.LoadSources(ini.LoadOptions{ + IgnoreInlineComment: true, + }, giteaLocale, renameGiteaForgejo(giteaLocale)) + if err != nil { + panic(err) + } + err = localeFile.SaveTo("options/locale/locale_" + locale) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/admin.go b/cmd/admin.go index 525bc2cfcd..3c09aa3175 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -6,7 +6,6 @@ package cmd import ( - "context" "errors" "fmt" "os" @@ -17,20 +16,14 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" - pwd "code.gitea.io/gitea/modules/password" repo_module "code.gitea.io/gitea/modules/repository" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/modules/util" auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/smtp" repo_service "code.gitea.io/gitea/services/repository" - user_service "code.gitea.io/gitea/services/user" "github.com/urfave/cli" ) @@ -49,142 +42,6 @@ var ( }, } - subcmdUser = cli.Command{ - Name: "user", - Usage: "Modify users", - Subcommands: []cli.Command{ - microcmdUserCreate, - microcmdUserList, - microcmdUserChangePassword, - microcmdUserDelete, - microcmdUserGenerateAccessToken, - }, - } - - microcmdUserList = cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", - }, - }, - } - - microcmdUserCreate = cli.Command{ - Name: "create", - Usage: "Create a new user in database", - Action: runCreateUser, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "name", - Usage: "Username. DEPRECATED: use username instead", - }, - cli.StringFlag{ - Name: "username", - Usage: "Username", - }, - cli.StringFlag{ - Name: "password", - Usage: "User password", - }, - cli.StringFlag{ - Name: "email", - Usage: "User email address", - }, - cli.BoolFlag{ - Name: "admin", - Usage: "User is an admin", - }, - cli.BoolFlag{ - Name: "random-password", - Usage: "Generate a random password for the user", - }, - cli.BoolFlag{ - Name: "must-change-password", - Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)", - }, - cli.IntFlag{ - Name: "random-password-length", - Usage: "Length of the random password to be generated", - Value: 12, - }, - cli.BoolFlag{ - Name: "access-token", - Usage: "Generate access token for the user", - }, - cli.BoolFlag{ - Name: "restricted", - Usage: "Make a restricted user account", - }, - }, - } - - microcmdUserChangePassword = cli.Command{ - Name: "change-password", - Usage: "Change a user's password", - Action: runChangePassword, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "username,u", - Value: "", - Usage: "The user to change password for", - }, - cli.StringFlag{ - Name: "password,p", - Value: "", - Usage: "New password to set for user", - }, - }, - } - - microcmdUserDelete = cli.Command{ - Name: "delete", - Usage: "Delete specific user by id, name or email", - Flags: []cli.Flag{ - cli.Int64Flag{ - Name: "id", - Usage: "ID of user of the user to delete", - }, - cli.StringFlag{ - Name: "username,u", - Usage: "Username of the user to delete", - }, - cli.StringFlag{ - Name: "email,e", - Usage: "Email of the user to delete", - }, - cli.BoolFlag{ - Name: "purge", - Usage: "Purge user, all their repositories, organizations and comments", - }, - }, - Action: runDeleteUser, - } - - microcmdUserGenerateAccessToken = cli.Command{ - Name: "generate-access-token", - Usage: "Generate a access token for a specific user", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "username,u", - Usage: "Username", - }, - cli.StringFlag{ - Name: "token-name,t", - Usage: "Token name", - Value: "gitea-admin", - }, - cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - }, - Action: runGenerateAccessToken, - } - subcmdRepoSyncReleases = cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", @@ -413,9 +270,9 @@ var ( Usage: "SMTP Authentication Type (PLAIN/LOGIN/CRAM-MD5) default PLAIN", }, cli.StringFlag{ - Name: "addr", + Name: "host", Value: "", - Usage: "SMTP Addr", + Usage: "SMTP Host", }, cli.IntFlag{ Name: "port", @@ -468,255 +325,6 @@ var ( } ) -func runChangePassword(c *cli.Context) error { - if err := argsSet(c, "username", "password"); err != nil { - return err - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - if len(c.String("password")) < setting.MinPasswordLength { - return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength) - } - - if !pwd.IsComplexEnough(c.String("password")) { - return errors.New("Password does not meet complexity requirements") - } - pwned, err := pwd.IsPwned(context.Background(), c.String("password")) - if err != nil { - return err - } - if pwned { - return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") - } - uname := c.String("username") - user, err := user_model.GetUserByName(ctx, uname) - if err != nil { - return err - } - if err = user.SetPassword(c.String("password")); err != nil { - return err - } - - if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil { - return err - } - - fmt.Printf("%s's password has been successfully updated!\n", user.Name) - return nil -} - -func runCreateUser(c *cli.Context) error { - if err := argsSet(c, "email"); err != nil { - return err - } - - if c.IsSet("name") && c.IsSet("username") { - return errors.New("Cannot set both --name and --username flags") - } - if !c.IsSet("name") && !c.IsSet("username") { - return errors.New("One of --name or --username flags must be set") - } - - if c.IsSet("password") && c.IsSet("random-password") { - return errors.New("cannot set both -random-password and -password flags") - } - - var username string - if c.IsSet("username") { - username = c.String("username") - } else { - username = c.String("name") - fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - var password string - if c.IsSet("password") { - password = c.String("password") - } else if c.IsSet("random-password") { - var err error - password, err = pwd.Generate(c.Int("random-password-length")) - if err != nil { - return err - } - fmt.Printf("generated random password is '%s'\n", password) - } else { - return errors.New("must set either password or random-password flag") - } - - // always default to true - changePassword := true - - // If this is the first user being created. - // Take it as the admin and don't force a password update. - if n := user_model.CountUsers(nil); n == 0 { - changePassword = false - } - - if c.IsSet("must-change-password") { - changePassword = c.Bool("must-change-password") - } - - restricted := util.OptionalBoolNone - - if c.IsSet("restricted") { - restricted = util.OptionalBoolOf(c.Bool("restricted")) - } - - u := &user_model.User{ - Name: username, - Email: c.String("email"), - Passwd: password, - IsAdmin: c.Bool("admin"), - MustChangePassword: changePassword, - } - - overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, - IsRestricted: restricted, - } - - if err := user_model.CreateUser(u, overwriteDefault); err != nil { - return fmt.Errorf("CreateUser: %w", err) - } - - if c.Bool("access-token") { - t := &auth_model.AccessToken{ - Name: "gitea-admin", - UID: u.ID, - } - - if err := auth_model.NewAccessToken(t); err != nil { - return err - } - - fmt.Printf("Access token was successfully created... %s\n", t.Token) - } - - fmt.Printf("New user '%s' has been successfully created!\n", username) - return nil -} - -func runListUsers(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - users, err := user_model.GetAllUsers() - if err != nil { - return err - } - - w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0) - - if c.IsSet("admin") { - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n") - for _, u := range users { - if u.IsAdmin { - fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive) - } - } - } else { - twofa := user_model.UserList(users).GetTwoFaStatus() - fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") - for _, u := range users { - fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID]) - } - - } - - w.Flush() - return nil -} - -func runDeleteUser(c *cli.Context) error { - if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { - return fmt.Errorf("You must provide the id, username or email of a user to delete") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - if err := storage.Init(); err != nil { - return err - } - - var err error - var user *user_model.User - if c.IsSet("email") { - user, err = user_model.GetUserByEmail(c.String("email")) - } else if c.IsSet("username") { - user, err = user_model.GetUserByName(ctx, c.String("username")) - } else { - user, err = user_model.GetUserByID(c.Int64("id")) - } - if err != nil { - return err - } - if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { - return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) - } - - if c.IsSet("id") && user.ID != c.Int64("id") { - return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id")) - } - - return user_service.DeleteUser(ctx, user, c.Bool("purge")) -} - -func runGenerateAccessToken(c *cli.Context) error { - if !c.IsSet("username") { - return fmt.Errorf("You must provide the username to generate a token for them") - } - - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - - user, err := user_model.GetUserByName(ctx, c.String("username")) - if err != nil { - return err - } - - t := &auth_model.AccessToken{ - Name: c.String("token-name"), - UID: user.ID, - } - - if err := auth_model.NewAccessToken(t); err != nil { - return err - } - - if c.Bool("raw") { - fmt.Printf("%s\n", t.Token) - } else { - fmt.Printf("Access token was successfully created: %s\n", t.Token) - } - - return nil -} - func runRepoSyncReleases(_ *cli.Context) error { ctx, cancel := installSignals() defer cancel() @@ -955,8 +563,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { } conf.Auth = c.String("auth-type") } - if c.IsSet("addr") { - conf.Addr = c.String("addr") + if c.IsSet("host") { + conf.Host = c.String("host") } if c.IsSet("port") { conf.Port = c.Int("port") diff --git a/cmd/admin_user.go b/cmd/admin_user.go new file mode 100644 index 0000000000..a442b8fe9c --- /dev/null +++ b/cmd/admin_user.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "github.com/urfave/cli" +) + +var subcmdUser = cli.Command{ + Name: "user", + Usage: "Modify users", + Subcommands: []cli.Command{ + microcmdUserCreate, + microcmdUserList, + microcmdUserChangePassword, + microcmdUserDelete, + microcmdUserGenerateAccessToken, + microcmdUserMustChangePassword, + }, +} diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go new file mode 100644 index 0000000000..7866bde912 --- /dev/null +++ b/cmd/admin_user_change_password.go @@ -0,0 +1,76 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "errors" + "fmt" + + user_model "code.gitea.io/gitea/models/user" + pwd "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli" +) + +var microcmdUserChangePassword = cli.Command{ + Name: "change-password", + Usage: "Change a user's password", + Action: runChangePassword, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "username,u", + Value: "", + Usage: "The user to change password for", + }, + cli.StringFlag{ + Name: "password,p", + Value: "", + Usage: "New password to set for user", + }, + }, +} + +func runChangePassword(c *cli.Context) error { + if err := argsSet(c, "username", "password"); err != nil { + return err + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + if len(c.String("password")) < setting.MinPasswordLength { + return fmt.Errorf("Password is not long enough. Needs to be at least %d", setting.MinPasswordLength) + } + + if !pwd.IsComplexEnough(c.String("password")) { + return errors.New("Password does not meet complexity requirements") + } + pwned, err := pwd.IsPwned(context.Background(), c.String("password")) + if err != nil { + return err + } + if pwned { + return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") + } + uname := c.String("username") + user, err := user_model.GetUserByName(ctx, uname) + if err != nil { + return err + } + if err = user.SetPassword(c.String("password")); err != nil { + return err + } + + if err = user_model.UpdateUserCols(ctx, user, "passwd", "passwd_hash_algo", "salt"); err != nil { + return err + } + + fmt.Printf("%s's password has been successfully updated!\n", user.Name) + return nil +} diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go new file mode 100644 index 0000000000..09eaad54be --- /dev/null +++ b/cmd/admin_user_create.go @@ -0,0 +1,169 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "errors" + "fmt" + "os" + + auth_model "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + pwd "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "github.com/urfave/cli" +) + +var microcmdUserCreate = cli.Command{ + Name: "create", + Usage: "Create a new user in database", + Action: runCreateUser, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name", + Usage: "Username. DEPRECATED: use username instead", + }, + cli.StringFlag{ + Name: "username", + Usage: "Username", + }, + cli.StringFlag{ + Name: "password", + Usage: "User password", + }, + cli.StringFlag{ + Name: "email", + Usage: "User email address", + }, + cli.BoolFlag{ + Name: "admin", + Usage: "User is an admin", + }, + cli.BoolFlag{ + Name: "random-password", + Usage: "Generate a random password for the user", + }, + cli.BoolFlag{ + Name: "must-change-password", + Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)", + }, + cli.IntFlag{ + Name: "random-password-length", + Usage: "Length of the random password to be generated", + Value: 12, + }, + cli.BoolFlag{ + Name: "access-token", + Usage: "Generate access token for the user", + }, + cli.BoolFlag{ + Name: "restricted", + Usage: "Make a restricted user account", + }, + }, +} + +func runCreateUser(c *cli.Context) error { + if err := argsSet(c, "email"); err != nil { + return err + } + + if c.IsSet("name") && c.IsSet("username") { + return errors.New("Cannot set both --name and --username flags") + } + if !c.IsSet("name") && !c.IsSet("username") { + return errors.New("One of --name or --username flags must be set") + } + + if c.IsSet("password") && c.IsSet("random-password") { + return errors.New("cannot set both -random-password and -password flags") + } + + var username string + if c.IsSet("username") { + username = c.String("username") + } else { + username = c.String("name") + fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + var password string + if c.IsSet("password") { + password = c.String("password") + } else if c.IsSet("random-password") { + var err error + password, err = pwd.Generate(c.Int("random-password-length")) + if err != nil { + return err + } + fmt.Printf("generated random password is '%s'\n", password) + } else { + return errors.New("must set either password or random-password flag") + } + + // always default to true + changePassword := true + + // If this is the first user being created. + // Take it as the admin and don't force a password update. + if n := user_model.CountUsers(nil); n == 0 { + changePassword = false + } + + if c.IsSet("must-change-password") { + changePassword = c.Bool("must-change-password") + } + + restricted := util.OptionalBoolNone + + if c.IsSet("restricted") { + restricted = util.OptionalBoolOf(c.Bool("restricted")) + } + + // default user visibility in app.ini + visibility := setting.Service.DefaultUserVisibilityMode + + u := &user_model.User{ + Name: username, + Email: c.String("email"), + Passwd: password, + IsAdmin: c.Bool("admin"), + MustChangePassword: changePassword, + Visibility: visibility, + } + + overwriteDefault := &user_model.CreateUserOverwriteOptions{ + IsActive: util.OptionalBoolTrue, + IsRestricted: restricted, + } + + if err := user_model.CreateUser(u, overwriteDefault); err != nil { + return fmt.Errorf("CreateUser: %w", err) + } + + if c.Bool("access-token") { + t := &auth_model.AccessToken{ + Name: "gitea-admin", + UID: u.ID, + } + + if err := auth_model.NewAccessToken(t); err != nil { + return err + } + + fmt.Printf("Access token was successfully created... %s\n", t.Token) + } + + fmt.Printf("New user '%s' has been successfully created!\n", username) + return nil +} diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go new file mode 100644 index 0000000000..43363783ed --- /dev/null +++ b/cmd/admin_user_delete.go @@ -0,0 +1,78 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + "strings" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/storage" + user_service "code.gitea.io/gitea/services/user" + + "github.com/urfave/cli" +) + +var microcmdUserDelete = cli.Command{ + Name: "delete", + Usage: "Delete specific user by id, name or email", + Flags: []cli.Flag{ + cli.Int64Flag{ + Name: "id", + Usage: "ID of user of the user to delete", + }, + cli.StringFlag{ + Name: "username,u", + Usage: "Username of the user to delete", + }, + cli.StringFlag{ + Name: "email,e", + Usage: "Email of the user to delete", + }, + cli.BoolFlag{ + Name: "purge", + Usage: "Purge user, all their repositories, organizations and comments", + }, + }, + Action: runDeleteUser, +} + +func runDeleteUser(c *cli.Context) error { + if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { + return fmt.Errorf("You must provide the id, username or email of a user to delete") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + if err := storage.Init(); err != nil { + return err + } + + var err error + var user *user_model.User + if c.IsSet("email") { + user, err = user_model.GetUserByEmail(c.String("email")) + } else if c.IsSet("username") { + user, err = user_model.GetUserByName(ctx, c.String("username")) + } else { + user, err = user_model.GetUserByID(c.Int64("id")) + } + if err != nil { + return err + } + if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { + return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) + } + + if c.IsSet("id") && user.ID != c.Int64("id") { + return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id")) + } + + return user_service.DeleteUser(ctx, user, c.Bool("purge")) +} diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go new file mode 100644 index 0000000000..196746ba56 --- /dev/null +++ b/cmd/admin_user_generate_access_token.go @@ -0,0 +1,69 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + + auth_model "code.gitea.io/gitea/models/auth" + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserGenerateAccessToken = cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "username,u", + Usage: "Username", + }, + cli.StringFlag{ + Name: "token-name,t", + Usage: "Token name", + Value: "gitea-admin", + }, + cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + }, + Action: runGenerateAccessToken, +} + +func runGenerateAccessToken(c *cli.Context) error { + if !c.IsSet("username") { + return fmt.Errorf("You must provide a username to generate a token for") + } + + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + user, err := user_model.GetUserByName(ctx, c.String("username")) + if err != nil { + return err + } + + t := &auth_model.AccessToken{ + Name: c.String("token-name"), + UID: user.ID, + } + + if err := auth_model.NewAccessToken(t); err != nil { + return err + } + + if c.Bool("raw") { + fmt.Printf("%s\n", t.Token) + } else { + fmt.Printf("Access token was successfully created: %s\n", t.Token) + } + + return nil +} diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go new file mode 100644 index 0000000000..85490331ed --- /dev/null +++ b/cmd/admin_user_list.go @@ -0,0 +1,60 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserList = cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, + }, +} + +func runListUsers(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + users, err := user_model.GetAllUsers() + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0) + + if c.IsSet("admin") { + fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n") + for _, u := range users { + if u.IsAdmin { + fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive) + } + } + } else { + twofa := user_model.UserList(users).GetTwoFaStatus() + fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n") + for _, u := range users { + fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID]) + } + } + + w.Flush() + return nil +} diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go new file mode 100644 index 0000000000..eb13fbcae5 --- /dev/null +++ b/cmd/admin_user_must_change_password.go @@ -0,0 +1,58 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "errors" + "fmt" + + user_model "code.gitea.io/gitea/models/user" + + "github.com/urfave/cli" +) + +var microcmdUserMustChangePassword = cli.Command{ + Name: "must-change-password", + Usage: "Set the must change password flag for the provided users or all users", + Action: runMustChangePassword, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "all,A", + Usage: "All users must change password, except those explicitly excluded with --exclude", + }, + cli.StringSliceFlag{ + Name: "exclude,e", + Usage: "Do not change the must-change-password flag for these users", + }, + cli.BoolFlag{ + Name: "unset", + Usage: "Instead of setting the must-change-password flag, unset it", + }, + }, +} + +func runMustChangePassword(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + if c.NArg() == 0 && !c.IsSet("all") { + return errors.New("either usernames or --all must be provided") + } + + mustChangePassword := !c.Bool("unset") + all := c.Bool("all") + exclude := c.StringSlice("exclude") + + if err := initDB(ctx); err != nil { + return err + } + + n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args(), exclude) + if err != nil { + return err + } + + fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword) + return nil +} diff --git a/cmd/serv.go b/cmd/serv.go index 06561f348a..d19020e5c3 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -149,13 +149,13 @@ func runServ(c *cli.Context) error { } switch key.Type { case asymkey_model.KeyTypeDeploy: - println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Gitea does not provide shell access.") + println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Forgejo does not provide shell access.") case asymkey_model.KeyTypePrincipal: - println("Hi there! You've successfully authenticated with the principal " + key.Content + ", but Gitea does not provide shell access.") + println("Hi there! You've successfully authenticated with the principal " + key.Content + ", but Forgejo does not provide shell access.") default: - println("Hi there, " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Gitea does not provide shell access.") + println("Hi there, " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Forgejo does not provide shell access.") } - println("If this is unexpected, please log in with password and setup Gitea under another user.") + println("If this is unexpected, please log in with password and setup Forgejo under another user.") return nil } else if c.Bool("debug") { log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND")) diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index ccda03fa92..0baba5902b 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -1,3 +1,4 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -19,17 +20,17 @@ import ( ) // EnvironmentPrefix environment variables prefixed with this represent ini values to write -const EnvironmentPrefix = "GITEA" +const prefixRegexpString = "^(FORGEJO|GITEA)" func main() { app := cli.NewApp() app.Name = "environment-to-ini" app.Usage = "Use provided environment to update configuration ini" - app.Description = `As a helper to allow docker users to update the gitea configuration + app.Description = `As a helper to allow docker users to update the forgejo configuration through the environment, this command allows environment variables to be mapped to values in the ini. - Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME" + Environment variables of the form "FORGEJO__SECTION_NAME__KEY_NAME" will be mapped to the ini section "[section_name]" and the key "KEY_NAME" with the value as provided. @@ -47,9 +48,8 @@ func main() { ... """ - You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" - and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found - on the configuration cheat sheet.` + You would set the environment variables: "FORGEJO__LOG_0x2E_CONSOLE__COLORIZE=false" + and "FORGEJO__LOG_0x2E_CONSOLE__STDERR=false".` app.Flags = []cli.Flag{ cli.StringFlag{ Name: "custom-path, C", @@ -77,7 +77,7 @@ func main() { }, cli.StringFlag{ Name: "prefix, p", - Value: EnvironmentPrefix, + Value: prefixRegexpString, Usage: "Environment prefix to look for - will be suffixed by __ (2 underscores)", }, } @@ -90,6 +90,19 @@ func main() { } } +func splitEnvironmentVariable(prefixRegexp *regexp.Regexp, kv string) (string, string) { + idx := strings.IndexByte(kv, '=') + if idx < 0 { + return "", "" + } + k := kv[:idx] + loc := prefixRegexp.FindStringIndex(k) + if loc == nil { + return "", "" + } + return k[loc[1]:], kv[idx+1:] +} + func runEnvironmentToIni(c *cli.Context) error { providedCustom := c.String("custom-path") providedConf := c.String("config") @@ -112,19 +125,13 @@ func runEnvironmentToIni(c *cli.Context) error { changed := false - prefix := c.String("prefix") + "__" + prefixRegexp := regexp.MustCompile(c.String("prefix") + "__") for _, kv := range os.Environ() { - idx := strings.IndexByte(kv, '=') - if idx < 0 { + eKey, value := splitEnvironmentVariable(prefixRegexp, kv) + if eKey == "" { continue } - eKey := kv[:idx] - value := kv[idx+1:] - if !strings.HasPrefix(eKey, prefix) { - continue - } - eKey = eKey[len(prefix):] sectionName, keyName := DecodeSectionKey(eKey) if len(keyName) == 0 { continue @@ -164,14 +171,11 @@ func runEnvironmentToIni(c *cli.Context) error { } if c.Bool("clear") { for _, kv := range os.Environ() { - idx := strings.IndexByte(kv, '=') - if idx < 0 { + eKey, _ := splitEnvironmentVariable(prefixRegexp, kv) + if eKey == "" { continue } - eKey := kv[:idx] - if strings.HasPrefix(eKey, prefix) { - _ = os.Unsetenv(eKey) - } + _ = os.Unsetenv(eKey) } } return nil diff --git a/contrib/environment-to-ini/environment-to-ini_test.go b/contrib/environment-to-ini/environment-to-ini_test.go new file mode 100644 index 0000000000..6abbb67eff --- /dev/null +++ b/contrib/environment-to-ini/environment-to-ini_test.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package main + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_splitEnvironmentVariable(t *testing.T) { + prefixRegexp := regexp.MustCompile(prefixRegexpString + "__") + k, v := splitEnvironmentVariable(prefixRegexp, "FORGEJO__KEY=VALUE") + assert.Equal(t, k, "KEY") + assert.Equal(t, v, "VALUE") + k, v = splitEnvironmentVariable(prefixRegexp, "nothing=interesting") + assert.Equal(t, k, "") + assert.Equal(t, v, "") +} diff --git a/contrib/systemd/gitea.service b/contrib/systemd/forgejo.service similarity index 59% rename from contrib/systemd/gitea.service rename to contrib/systemd/forgejo.service index d205c6ee8b..04ef69adc0 100644 --- a/contrib/systemd/gitea.service +++ b/contrib/systemd/forgejo.service @@ -1,5 +1,5 @@ [Unit] -Description=Gitea (Git with a cup of tea) +Description=Forgejo (Beyond coding. We forge.) After=syslog.target After=network.target ### @@ -25,21 +25,21 @@ After=network.target # If using socket activation for main http/s ### # -#After=gitea.main.socket -#Requires=gitea.main.socket +#After=forgejo.main.socket +#Requires=forgejo.main.socket # ### -# (You can also provide gitea an http fallback and/or ssh socket too) +# (You can also provide forgejo an http fallback and/or ssh socket too) # -# An example of /etc/systemd/system/gitea.main.socket +# An example of /etc/systemd/system/forgejo.main.socket ### ## ## [Unit] -## Description=Gitea Web Socket -## PartOf=gitea.service +## Description=Forgejo Web Socket +## PartOf=forgejo.service ## ## [Socket] -## Service=gitea.service +## Service=forgejo.service ## ListenStream= ## NoDelay=true ## @@ -55,28 +55,28 @@ RestartSec=2s Type=simple User=git Group=git -WorkingDirectory=/var/lib/gitea/ -# If using Unix socket: tells systemd to create the /run/gitea folder, which will contain the gitea.sock file -# (manually creating /run/gitea doesn't work, because it would not persist across reboots) -#RuntimeDirectory=gitea -ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini +WorkingDirectory=/var/lib/forgejo/ +# If using Unix socket: tells systemd to create the /run/forgejo folder, which will contain the forgejo.sock file +# (manually creating /run/forgejo doesn't work, because it would not persist across reboots) +#RuntimeDirectory=forgejo +ExecStart=/usr/local/bin/forgejo web --config /etc/forgejo/app.ini Restart=always -Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea +Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/forgejo # If you install Git to directory prefix other than default PATH (which happens # for example if you install other versions of Git side-to-side with # distribution version), uncomment below line and add that prefix to PATH # Don't forget to place git-lfs binary on the PATH below if you want to enable # Git LFS support #Environment=PATH=/path/to/git/bin:/bin:/sbin:/usr/bin:/usr/sbin -# If you want to bind Gitea to a port below 1024, uncomment -# the two values below, or use socket activation to pass Gitea its ports as above +# If you want to bind Forgejo to a port below 1024, uncomment +# the two values below, or use socket activation to pass Forgejo its ports as above ### #CapabilityBoundingSet=CAP_NET_BIND_SERVICE #AmbientCapabilities=CAP_NET_BIND_SERVICE ### # In some cases, when using CapabilityBoundingSet and AmbientCapabilities option, you may want to -# set the following value to false to allow capabilities to be applied on gitea process. The following -# value if set to true sandboxes gitea service and prevent any processes from running with privileges +# set the following value to false to allow capabilities to be applied on Forgejo process. The following +# value if set to true sandboxes Forgejo service and prevent any processes from running with privileges # in the host user namespace. ### #PrivateUsers=false diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index ec0b7c5235..39ee594549 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -331,7 +331,7 @@ USER = root ;; SQLite Configuration ;; ;DB_TYPE = sqlite3 -;PATH= ; defaults to data/gitea.db +;PATH= ; defaults to data/forgejo.db ;SQLITE_TIMEOUT = ; Query timeout defaults to: 500 ;SQLITE_JOURNAL_MODE = ; defaults to sqlite database default (often DELETE), can be used to enable WAL mode. https://www.sqlite.org/pragma.html#pragma_journal_mode ;; @@ -439,8 +439,8 @@ INTERNAL_TOKEN= ;;Classes include "lower,upper,digit,spec" ;PASSWORD_COMPLEXITY = off ;; -;; Password Hash algorithm, either "argon2", "pbkdf2", "scrypt" or "bcrypt" -;PASSWORD_HASH_ALGO = pbkdf2 +;; Password Hash algorithm, either "argon2", "pbkdf2"/"pbkdf2_v2", "pbkdf2_hi", "scrypt" or "bcrypt" +;PASSWORD_HASH_ALGO = pbkdf2_hi ;; ;; Set false to allow JavaScript to read CSRF cookie ;CSRF_COOKIE_HTTP_ONLY = true @@ -996,6 +996,9 @@ ROUTER = console ;; ;; Add co-authored-by and co-committed-by trailers if committer does not match author ;ADD_CO_COMMITTER_TRAILERS = true +;; +;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply +;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = true ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1550,7 +1553,7 @@ ROUTER = console ;; Prefix displayed before subject in mail ;SUBJECT_PREFIX = ;; -;; Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy". +;; Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". ;; - sendmail: use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems. ;; - dummy: send email messages to the log as a testing phase. ;; If your provider does not explicitly say which protocol it uses but does provide a port, @@ -2109,6 +2112,7 @@ ROUTER = console ;ENABLE_SUCCESS_NOTICE = false ;SCHEDULE = @every 168h ;HTTP_ENDPOINT = https://dl.gitea.io/gitea/version.json +;DOMAIN_ENDPOINT = release.forgejo.org ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docker/root/etc/s6/gitea/setup b/docker/root/etc/s6/gitea/setup index d8f6a3b319..958d50a798 100755 --- a/docker/root/etc/s6/gitea/setup +++ b/docker/root/etc/s6/gitea/setup @@ -24,7 +24,7 @@ if [ ! -f ${GITEA_CUSTOM}/conf/app.ini ]; then fi # Substitute the environment variables in the template - APP_NAME=${APP_NAME:-"Gitea: Git with a cup of tea"} \ + APP_NAME=${APP_NAME:-"Forgejo: Beyond coding. We forge."} \ RUN_MODE=${RUN_MODE:-"prod"} \ DOMAIN=${DOMAIN:-"localhost"} \ SSH_DOMAIN=${SSH_DOMAIN:-"localhost"} \ diff --git a/docker/rootless/usr/local/bin/docker-setup.sh b/docker/rootless/usr/local/bin/docker-setup.sh index feab02a379..b480685863 100755 --- a/docker/rootless/usr/local/bin/docker-setup.sh +++ b/docker/rootless/usr/local/bin/docker-setup.sh @@ -26,7 +26,7 @@ if [ ! -f ${GITEA_APP_INI} ]; then fi # Substitute the environment variables in the template - APP_NAME=${APP_NAME:-"Gitea: Git with a cup of tea"} \ + APP_NAME=${APP_NAME:-"Forgejo: Beyond coding. We forge."} \ RUN_MODE=${RUN_MODE:-"prod"} \ RUN_USER=${USER:-"git"} \ SSH_DOMAIN=${SSH_DOMAIN:-"localhost"} \ diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 3fd853fb14..03e6566b6f 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -101,6 +101,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY`: **true**: In default merge messages only include approvers who are officially allowed to review. - `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`: **false**: In default squash-merge messages include the commit message of all commits comprising the pull request. - `ADD_CO_COMMITTER_TRAILERS`: **true**: Add co-authored-by and co-committed-by trailers to merge commit messages if committer does not match author. +- `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY`: **true**: PR patches are tested using a three-way merge method to discover if there are conflicts. If this setting is set to **true**, conflicting patches will be retested using `git apply` - This was the previous behaviour in 1.18 (and earlier) but is somewhat inefficient. Please report if you find that this setting is required. ### Repository - Issue (`repository.issue`) @@ -522,7 +523,21 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o - `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server. - `INTERNAL_TOKEN`: **\**: Secret used to validate communication within Gitea binary. - `INTERNAL_TOKEN_URI`: ****: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`) -- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, scrypt, bcrypt\], argon2 will spend more memory than others. +- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory. + - Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended. + - The hash functions may be tuned by using `$` after the algorithm: + - `argon2$

$` + - The defaults are: + - `argon2`: `argon2$2$65536$8$50` + - `bcrypt`: `bcrypt$10` + - `pbkdf2`: `pbkdf2$320000$50` + - `pbkdf2_v1`: `pbkdf2$10000$50` + - `pbkdf2_v2`: `pbkdf2$320000$50` + - `scrypt`: `scrypt$65536$16$2$50` + - Adjusting the algorithm parameters using this functionality is done at your own risk. - `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie. - `MIN_PASSWORD_LENGTH`: **6**: Minimum password length for new users. - `PASSWORD_COMPLEXITY`: **off**: Comma separated list of character classes required to pass minimum complexity. If left empty or no valid values are specified, checking is disabled (off): @@ -672,7 +687,7 @@ and [Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md) - `ENABLED`: **false**: Enable to use a mail service. -- `PROTOCOL`: **\**: Mail server protocol. One of "smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._ +- `PROTOCOL`: **\**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._ - SMTP family, if your provider does not explicitly say which protocol it uses but does provide a port, you can set SMTP_PORT instead and this will be inferred. - **sendmail** Use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems. - **dummy** Send email messages to the log as a testing phase. @@ -734,9 +749,9 @@ and - `GRAVATAR_SOURCE`: **gravatar**: Can be `gravatar`, `duoshuo` or anything like `http://cn.gravatar.com/avatar/`. -- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only. +- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only. **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure. - `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see - [http://www.libravatar.org](http://www.libravatar.org)). + [http://www.libravatar.org](http://www.libravatar.org)). **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure. - `AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`. - `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. @@ -968,6 +983,7 @@ Default templates for project boards: - `ENABLE_SUCCESS_NOTICE`: **true**: Set to false to switch off success notices. - `SCHEDULE`: **@every 168h**: Cron syntax for scheduling a work, e.g. `@every 168h`. - `HTTP_ENDPOINT`: **https://dl.gitea.io/gitea/version.json**: the endpoint that Gitea will check for newer versions +- `DOMAIN_ENDPOINT`: **release.forgejo.org**: the domain that, if specified, Gitea will check for newer versions. This is preferred over `HTTP_ENDPOINT`. #### Cron - Delete all old system notices from database ('cron.delete_old_system_notices') diff --git a/docs/content/doc/packages/pypi.en-us.md b/docs/content/doc/packages/pypi.en-us.md index 588df71d60..ec2475aea3 100644 --- a/docs/content/doc/packages/pypi.en-us.md +++ b/docs/content/doc/packages/pypi.en-us.md @@ -77,6 +77,8 @@ For example: pip install --index-url https://testuser:password123@gitea.example.com/api/packages/testuser/pypi/simple --no-deps test_package ``` +You can use `--extra-index-url` instead of `--index-url` but that makes you vulnerable to dependency confusion attacks because `pip` checks the official PyPi repository for the package before it checks the specified custom repository. Read the `pip` docs for more information. + ## Supported commands ``` diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index 5f05bc4c3b..9a7b2dd023 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -99,6 +99,13 @@ Admin operations: - `--password value`, `-p value`: New password. Required. - Examples: - `gitea admin user change-password --username myname --password asecurepassword` + - `must-change-password`: + - Args: + - `[username...]`: Users that must change their passwords + - Options: + - `--all`, `-A`: Force a password change for all users + - `--exclude username`, `-e username`: Exclude the given user. Can be set multiple times. + - `--unset`: Revoke forced password change for the given users - `regenerate` - Options: - `hooks`: Regenerate Git Hooks for all repositories diff --git a/go.mod b/go.mod index 408249880c..fe681f0184 100644 --- a/go.mod +++ b/go.mod @@ -15,8 +15,8 @@ require ( github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/NYTimes/gziphandler v1.1.1 github.com/PuerkitoBio/goquery v1.8.0 - github.com/alecthomas/chroma/v2 v2.3.0 - github.com/blevesearch/bleve/v2 v2.3.4 + github.com/alecthomas/chroma/v2 v2.4.0 + github.com/blevesearch/bleve/v2 v2.3.5 github.com/buildkite/terminal-to-html/v3 v3.7.0 github.com/caddyserver/certmagic v0.17.2 github.com/chi-middleware/proxy v1.1.1 @@ -75,6 +75,8 @@ require ( github.com/niklasfasching/go-org v1.6.5 github.com/oliamb/cutter v0.2.2 github.com/olivere/elastic/v7 v7.0.32 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0-rc2 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 github.com/prometheus/client_golang v1.13.0 @@ -94,11 +96,11 @@ require ( github.com/yuin/goldmark-meta v1.1.0 go.jolheiser.com/hcaptcha v0.0.4 go.jolheiser.com/pwn v0.0.3 - golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be - golang.org/x/net v0.0.0-20220927171203-f486391704dc + golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891 + golang.org/x/net v0.2.0 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 - golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec - golang.org/x/text v0.3.8 + golang.org/x/sys v0.2.0 + golang.org/x/text v0.4.0 golang.org/x/tools v0.1.12 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 @@ -129,21 +131,21 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bits-and-blooms/bitset v1.3.3 // indirect - github.com/blevesearch/bleve_index_api v1.0.3 // indirect - github.com/blevesearch/geo v0.1.14 // indirect + github.com/blevesearch/bleve_index_api v1.0.4 // indirect + github.com/blevesearch/geo v0.1.15 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.1.2 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.1.3 // indirect github.com/blevesearch/segment v0.9.0 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.1 // indirect - github.com/blevesearch/vellum v1.0.8 // indirect - github.com/blevesearch/zapx/v11 v11.3.5 // indirect - github.com/blevesearch/zapx/v12 v12.3.5 // indirect - github.com/blevesearch/zapx/v13 v13.3.5 // indirect - github.com/blevesearch/zapx/v14 v14.3.5 // indirect - github.com/blevesearch/zapx/v15 v15.3.5 // indirect + github.com/blevesearch/vellum v1.0.9 // indirect + github.com/blevesearch/zapx/v11 v11.3.6 // indirect + github.com/blevesearch/zapx/v12 v12.3.6 // indirect + github.com/blevesearch/zapx/v13 v13.3.6 // indirect + github.com/blevesearch/zapx/v14 v14.3.6 // indirect + github.com/blevesearch/zapx/v15 v15.3.6 // indirect github.com/boombuler/barcode v1.0.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect @@ -285,6 +287,7 @@ require ( go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 // indirect @@ -302,6 +305,8 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142 replace github.com/satori/go.uuid v1.2.0 => github.com/gofrs/uuid v4.2.0+incompatible +replace github.com/blevesearch/zapx/v15 v15.3.6 => github.com/zeripath/zapx/v15 v15.3.6-alignment-fix-2 + exclude github.com/gofrs/uuid v3.2.0+incompatible exclude github.com/gofrs/uuid v4.0.0+incompatible diff --git a/go.sum b/go.sum index 65841b8ec3..dcb1932c8b 100644 --- a/go.sum +++ b/go.sum @@ -149,7 +149,6 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I= -github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/RoaringBitmap/roaring v1.2.1 h1:58/LJlg/81wfEHd5L9qsHduznOIhyv4qb1yWcSvVq9A= github.com/RoaringBitmap/roaring v1.2.1/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -160,9 +159,10 @@ github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/assert/v2 v2.2.0 h1:f6L/b7KE2bfA+9O4FL3CM/xJccDEwPVYd5fALBiuwvw= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.3.0 h1:83xfxrnjv8eK+Cf8qZDzNo3PPF9IbTWHs7z28GY6D0U= -github.com/alecthomas/chroma/v2 v2.3.0/go.mod h1:mZxeWZlxP2Dy+/8cBob2PYd8O2DwNAzave5AY7A2eQw= +github.com/alecthomas/chroma/v2 v2.4.0 h1:Loe2ZjT5x3q1bcWwemqyqEi8p11/IV/ncFCeLYDpWC4= +github.com/alecthomas/chroma/v2 v2.4.0/go.mod h1:6kHzqF5O6FUSJzBXW7fXELjb+e+7OXW4UpoPqMO7IBQ= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= @@ -225,52 +225,47 @@ github.com/bits-and-blooms/bitset v1.3.3/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM= -github.com/blevesearch/bleve/v2 v2.3.4 h1:SSb7/cwGzo85LWX1jchIsXM8ZiNNMX3shT5lROM63ew= -github.com/blevesearch/bleve/v2 v2.3.4/go.mod h1:Ot0zYum8XQRfPcwhae8bZmNyYubynsoMjVvl1jPqL30= +github.com/blevesearch/bleve/v2 v2.3.5 h1:1wuR7eB8Fk9UaCaBUfnQt5V7zIpi4VDok9ExN7Rl+/8= +github.com/blevesearch/bleve/v2 v2.3.5/go.mod h1:FneKGHMRrCLrp4X9+iy3wlBqgM2ALucg7bp8jUuAi/s= github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= -github.com/blevesearch/bleve_index_api v1.0.3 h1:DDSWaPXOZZJ2BB73ZTWjKxydAugjwywcqU+91AAqcAg= github.com/blevesearch/bleve_index_api v1.0.3/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= -github.com/blevesearch/geo v0.1.13/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc= -github.com/blevesearch/geo v0.1.14 h1:TTDpJN6l9ck/cUYbXSn4aCElNls0Whe44rcQKsB7EfU= -github.com/blevesearch/geo v0.1.14/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc= -github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= +github.com/blevesearch/bleve_index_api v1.0.4 h1:mtlzsyJjMIlDngqqB1mq8kPryUMIuEVVbRbJHOWEexU= +github.com/blevesearch/bleve_index_api v1.0.4/go.mod h1:YXMDwaXFFXwncRS8UobWs7nvo0DmusriM1nztTlj1ms= +github.com/blevesearch/geo v0.1.15 h1:0NybEduqE5fduFRYiUKF0uqybAIFKXYjkBdXKYn7oA4= +github.com/blevesearch/geo v0.1.15/go.mod h1:cRIvqCdk3cgMhGeHNNe6yPzb+w56otxbfo1FBJfR2Pc= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= -github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU= -github.com/blevesearch/scorch_segment_api/v2 v2.1.2 h1:TAte9VZLWda5WAVlZTTZ+GCzEHqGJb4iB2aiZSA6Iv8= -github.com/blevesearch/scorch_segment_api/v2 v2.1.2/go.mod h1:rvoQXZGq8drq7vXbNeyiRzdEOwZkjkiYGf1822i6CRA= +github.com/blevesearch/scorch_segment_api/v2 v2.1.3 h1:2UzpR2dR5DvSZk8tVJkcQ7D5xhoK/UBelYw8ttBHrRQ= +github.com/blevesearch/scorch_segment_api/v2 v2.1.3/go.mod h1:eZrfp1y+lUh+DzFjUcTBUSnKGuunyFIpBIvqYVzJfvc= github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= -github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/upsidedown_store_api v1.0.1 h1:1SYRwyoFLwG3sj0ed89RLtM15amfX2pXlYbFOnF8zNU= github.com/blevesearch/upsidedown_store_api v1.0.1/go.mod h1:MQDVGpHZrpe3Uy26zJBf/a8h0FZY6xJbthIMm8myH2Q= github.com/blevesearch/vellum v1.0.3/go.mod h1:2u5ax02KeDuNWu4/C+hVQMD6uLN4txH1JbtpaDNLJRo= github.com/blevesearch/vellum v1.0.4/go.mod h1:cMhywHI0de50f7Nj42YgvyD6bFJ2WkNRvNBlNMrEVgY= -github.com/blevesearch/vellum v1.0.8 h1:iMGh4lfxza4BnWO/UJTMPlI3HsK9YawjPv+TteVa9ck= -github.com/blevesearch/vellum v1.0.8/go.mod h1:+cpRi/tqq49xUYSQN2P7A5zNSNrS+MscLeeaZ3J46UA= +github.com/blevesearch/vellum v1.0.9 h1:PL+NWVk3dDGPCV0hoDu9XLLJgqU4E5s/dOeEJByQ2uQ= +github.com/blevesearch/vellum v1.0.9/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= github.com/blevesearch/zapx/v11 v11.2.0/go.mod h1:gN/a0alGw1FZt/YGTo1G6Z6XpDkeOfujX5exY9sCQQM= -github.com/blevesearch/zapx/v11 v11.3.5 h1:eBQWQ7huA+mzm0sAGnZDwgGGli7S45EO+N+ObFWssbI= -github.com/blevesearch/zapx/v11 v11.3.5/go.mod h1:5UdIa/HRMdeRCiLQOyFESsnqBGiip7vQmYReA9toevU= +github.com/blevesearch/zapx/v11 v11.3.6 h1:50jET4HUJ6eCqGxdhUt+mjybMvEX2MWyqLGtCx3yUgc= +github.com/blevesearch/zapx/v11 v11.3.6/go.mod h1:B0CzJRj/pS7hJIroflRtFsa9mRHpMSucSgre0FVINns= github.com/blevesearch/zapx/v12 v12.2.0/go.mod h1:fdjwvCwWWwJW/EYTYGtAp3gBA0geCYGLcVTtJEZnY6A= -github.com/blevesearch/zapx/v12 v12.3.5 h1:5pX2hU+R1aZihT7ac1dNWh1n4wqkIM9pZzWp0ANED9s= -github.com/blevesearch/zapx/v12 v12.3.5/go.mod h1:ANcthYRZQycpbRut/6ArF5gP5HxQyJqiFcuJCBju/ss= +github.com/blevesearch/zapx/v12 v12.3.6 h1:G304NHBLgQeZ+IHK/XRCM0nhHqAts8MEvHI6LhoDNM4= +github.com/blevesearch/zapx/v12 v12.3.6/go.mod h1:iYi7tIKpauwU5os5wTxJITixr5Km21Hl365otMwdaP0= github.com/blevesearch/zapx/v13 v13.2.0/go.mod h1:o5rAy/lRS5JpAbITdrOHBS/TugWYbkcYZTz6VfEinAQ= -github.com/blevesearch/zapx/v13 v13.3.5 h1:eJ3gbD+Nu8p36/O6lhfdvWQ4pxsGYSuTOBrLLPVWJ74= -github.com/blevesearch/zapx/v13 v13.3.5/go.mod h1:FV+dRnScFgKnRDIp08RQL4JhVXt1x2HE3AOzqYa6fjo= +github.com/blevesearch/zapx/v13 v13.3.6 h1:vavltQHNdjQezhLZs5nIakf+w/uOa1oqZxB58Jy/3Ig= +github.com/blevesearch/zapx/v13 v13.3.6/go.mod h1:X+FsTwCU8qOHtK0d/ArvbOH7qiIgViSQ1GQvcR6LSkI= github.com/blevesearch/zapx/v14 v14.2.0/go.mod h1:GNgZusc1p4ot040cBQMRGEZobvwjCquiEKYh1xLFK9g= -github.com/blevesearch/zapx/v14 v14.3.5 h1:hEvVjZaagFCvOUJrlFQ6/Z6Jjy0opM3g7TMEo58TwP4= -github.com/blevesearch/zapx/v14 v14.3.5/go.mod h1:954A/eKFb+pg/ncIYWLWCKY+mIjReM9FGTGIO2Wu1cU= +github.com/blevesearch/zapx/v14 v14.3.6 h1:b9lub7TvcwUyJxK/cQtnN79abngKxsI7zMZnICU0WhE= +github.com/blevesearch/zapx/v14 v14.3.6/go.mod h1:9X8W3XoikagU0rwcTqwZho7p9cC7m7zhPZO94S4wUvM= github.com/blevesearch/zapx/v15 v15.2.0/go.mod h1:MmQceLpWfME4n1WrBFIwplhWmaQbQqLQARpaKUEOs/A= -github.com/blevesearch/zapx/v15 v15.3.5 h1:NVD0qq8vRk66ImJn1KloXT5ckqPDUZT7VbVJs9jKlac= -github.com/blevesearch/zapx/v15 v15.3.5/go.mod h1:QMUh2hXCaYIWFKPYGavq/Iga2zbHWZ9DZAa9uFbWyvg= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -361,7 +356,6 @@ github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFl github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401 h1:4KDlx3vjalrHD/EfsjCpV91HNX3JPaIqRtt83zZ7x+Y= github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= -github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -830,6 +824,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= @@ -1179,6 +1174,10 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1487,6 +1486,8 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87/go.m github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/zeripath/zapx/v15 v15.3.6-alignment-fix-2 h1:IRB+69BV7fTT5ccw35ca7TCBe2b7dm5Q5y5tUMQmCvU= +github.com/zeripath/zapx/v15 v15.3.6-alignment-fix-2/go.mod h1:5DbhhDTGtuQSns1tS2aJxJLPc91boXCvjOMeCLD1saM= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= @@ -1608,8 +1609,8 @@ golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891 h1:WhEPFM1Ck5gaKybeSWvzI7Y/cd8K9K5tJGRxXMACOBA= +golang.org/x/crypto v0.2.1-0.20221112162523-6fad3dfc1891/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1721,8 +1722,8 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= -golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1762,7 +1763,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1876,13 +1878,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1892,8 +1894,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/models/activities/action.go b/models/activities/action.go index 147511edec..5ff0079a6b 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -272,7 +272,7 @@ func (a *Action) GetRefLink() string { return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix)) case strings.HasPrefix(a.RefName, git.TagPrefix): return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix)) - case len(a.RefName) == 40 && git.IsValidSHAPattern(a.RefName): + case len(a.RefName) == git.SHAFullLength && git.IsValidSHAPattern(a.RefName): return a.GetRepoLink() + "/src/commit/" + a.RefName default: // FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here. diff --git a/models/activities/notification.go b/models/activities/notification.go index 5748b807a0..6691c222cb 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -157,7 +157,7 @@ func CreateRepoTransferNotification(doer, newOwner *user_model.User, repo *repo_ } for i := range users { notify = append(notify, &Notification{ - UserID: users[i].ID, + UserID: i, RepoID: repo.ID, Status: NotificationStatusUnread, UpdatedBy: doer.ID, diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go index 83774533aa..cc22eab9f8 100644 --- a/models/asymkey/gpg_key.go +++ b/models/asymkey/gpg_key.go @@ -68,8 +68,16 @@ func (key *GPGKey) PaddedKeyID() string { if len(key.KeyID) > 15 { return key.KeyID } + return PaddedKeyID(key.KeyID) +} + +// PaddedKeyID show KeyID padded to 16 characters +func PaddedKeyID(keyID string) string { + if len(keyID) > 15 { + return keyID + } zeros := "0000000000000000" - return zeros[0:16-len(key.KeyID)] + key.KeyID + return zeros[0:16-len(keyID)] + keyID } // ListGPGKeys returns a list of public keys belongs to given user. diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index 418e9b9ccc..045a992e6d 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -20,8 +20,12 @@ import ( "code.gitea.io/gitea/modules/setting" ) -// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar -const DefaultAvatarPixelSize = 28 +const ( + // DefaultAvatarClass is the default class of a rendered avatar + DefaultAvatarClass = "ui avatar vm" + // DefaultAvatarPixelSize is the default size in pixels of a rendered avatar + DefaultAvatarPixelSize = 28 +) // EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records) type EmailHash struct { @@ -150,10 +154,10 @@ func generateEmailAvatarLink(email string, size int, final bool) string { return DefaultAvatarLink() } - enableFederatedAvatar, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar) + enableFederatedAvatar := system_model.GetSettingBool(system_model.KeyPictureEnableFederatedAvatar) var err error - if enableFederatedAvatar != nil && enableFederatedAvatar.GetValueBool() && system_model.LibravatarService != nil { + if enableFederatedAvatar && system_model.LibravatarService != nil { emailHash := saveEmailHash(email) if final { // for final link, we can spend more time on slow external query @@ -171,8 +175,8 @@ func generateEmailAvatarLink(email string, size int, final bool) string { return urlStr } - disableGravatar, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar) - if disableGravatar != nil && !disableGravatar.GetValueBool() { + disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar) + if !disableGravatar { // copy GravatarSourceURL, because we will modify its Path. avatarURLCopy := *system_model.GravatarSourceURL avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) diff --git a/models/db/context.go b/models/db/context.go index 4fd35200cf..0d8d0a5bc4 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -24,8 +24,10 @@ type contextKey struct { } // enginedContextKey is a context key. It is used with context.Value() to get the current Engined for the context -var enginedContextKey = &contextKey{"engined"} -var _ Engined = &Context{} +var ( + enginedContextKey = &contextKey{"engined"} + _ Engined = &Context{} +) // Context represents a db context type Context struct { diff --git a/models/db/index.go b/models/db/index.go index 58a976ad52..f64bf6bfb5 100644 --- a/models/db/index.go +++ b/models/db/index.go @@ -8,6 +8,9 @@ import ( "context" "errors" "fmt" + "strconv" + + "code.gitea.io/gitea/modules/setting" ) // ResourceIndex represents a resource index which could be used as issue/release and others @@ -24,11 +27,6 @@ var ( ErrGetResourceIndexFailed = errors.New("get resource index failed") ) -const ( - // MaxDupIndexAttempts max retry times to create index - MaxDupIndexAttempts = 3 -) - // SyncMaxResourceIndex sync the max index with the resource func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) { e := GetEngine(ctx) @@ -61,8 +59,25 @@ func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxInd return nil } +func postgresGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { + res, err := GetEngine(ctx).Query(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ + "VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1 RETURNING max_index", + tableName, tableName), groupID) + if err != nil { + return 0, err + } + if len(res) == 0 { + return 0, ErrGetResourceIndexFailed + } + return strconv.ParseInt(string(res[0]["max_index"]), 10, 64) +} + // GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { + if setting.Database.UsePostgreSQL { + return postgresGetNextResourceIndex(ctx, tableName, groupID) + } + e := GetEngine(ctx) // try to update the max_index to next value, and acquire the write-lock for the record diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 59ab618340..8550dd4ef4 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -544,3 +544,16 @@ repo_id: 51 type: 2 created_unix: 946684810 + +- + id: 80 + repo_id: 31 + type: 1 + created_unix: 946684810 + +- + id: 81 + repo_id: 31 + type: 3 + config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index f09953be7e..386904683a 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -24,7 +24,7 @@ fork_id: 0 is_template: false template_id: 0 - size: 0 + size: 6708 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index ea47a33f1c..dd434d78a9 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -140,3 +140,14 @@ num_members: 1 includes_all_repositories: false can_create_org_repo: false + +- + id: 14 + org_id: 3 + lower_name: teamcreaterepo + name: teamCreateRepo + authorize: 2 # write + num_repos: 0 + num_members: 1 + includes_all_repositories: false + can_create_org_repo: true diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml index 845741effd..de4f29d977 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -93,3 +93,9 @@ org_id: 19 team_id: 6 uid: 31 + +- + id: 17 + org_id: 3 + team_id: 14 + uid: 2 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 0e3348e146..bdc97bf538 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -8,8 +8,8 @@ email: user1@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user1 @@ -45,8 +45,8 @@ email: user2@example.com keep_email_private: true email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user2 @@ -82,8 +82,8 @@ email: user3@example.com keep_email_private: false email_notifications_preference: onmention - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user3 @@ -104,7 +104,7 @@ num_following: 0 num_stars: 0 num_repos: 3 - num_teams: 4 + num_teams: 5 num_members: 3 visibility: 0 repo_admin_change_team_access: false @@ -119,8 +119,8 @@ email: user4@example.com keep_email_private: false email_notifications_preference: onmention - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user4 @@ -156,8 +156,8 @@ email: user5@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user5 @@ -193,8 +193,8 @@ email: user6@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user6 @@ -230,8 +230,8 @@ email: user7@example.com keep_email_private: false email_notifications_preference: disabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user7 @@ -267,8 +267,8 @@ email: user8@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user8 @@ -304,8 +304,8 @@ email: user9@example.com keep_email_private: false email_notifications_preference: onmention - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user9 @@ -341,8 +341,8 @@ email: user10@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user10 @@ -378,8 +378,8 @@ email: user11@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user11 @@ -415,8 +415,8 @@ email: user12@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user12 @@ -452,8 +452,8 @@ email: user13@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user13 @@ -489,8 +489,8 @@ email: user14@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user14 @@ -526,8 +526,8 @@ email: user15@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user15 @@ -563,8 +563,8 @@ email: user16@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user16 @@ -600,8 +600,8 @@ email: user17@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user17 @@ -637,8 +637,8 @@ email: user18@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user18 @@ -674,8 +674,8 @@ email: user19@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user19 @@ -711,8 +711,8 @@ email: user20@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user20 @@ -748,8 +748,8 @@ email: user21@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user21 @@ -785,8 +785,8 @@ email: limited_org@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: limited_org @@ -822,8 +822,8 @@ email: privated_org@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: privated_org @@ -859,8 +859,8 @@ email: user24@example.com keep_email_private: true email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user24 @@ -896,8 +896,8 @@ email: org25@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: org25 @@ -933,8 +933,8 @@ email: org26@example.com keep_email_private: false email_notifications_preference: onmention - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: org26 @@ -970,8 +970,8 @@ email: user27@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user27 @@ -1007,8 +1007,8 @@ email: user28@example.com keep_email_private: true email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user28 @@ -1044,8 +1044,8 @@ email: user29@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user29 @@ -1081,8 +1081,8 @@ email: user30@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user30 @@ -1118,8 +1118,8 @@ email: user31@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user31 @@ -1155,7 +1155,7 @@ email: user32@example.com keep_email_private: false email_notifications_preference: enabled - passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a + passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f47017 passwd_hash_algo: argon2 must_change_password: false login_source: 0 @@ -1192,8 +1192,8 @@ email: user33@example.com keep_email_private: false email_notifications_preference: enabled - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b - passwd_hash_algo: argon2 + passwd: e82bc8ae42a53b98c3bd0f941aacc4aa2a264407534b0a11bf270137f67af912f694b67951f92148c45f91717e1478ca7889 + passwd_hash_algo: pbkdf2$50000$50 must_change_password: false login_source: 0 login_name: user33 diff --git a/models/git/commit_status.go b/models/git/commit_status.go index 620baa036c..9e7fb5f805 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -7,8 +7,10 @@ package git import ( "context" "crypto/sha1" + "errors" "fmt" "net/url" + "strconv" "strings" "time" @@ -49,79 +51,67 @@ func init() { db.RegisterModel(new(CommitStatusIndex)) } -// upsertCommitStatusIndex the function will not return until it acquires the lock or receives an error. -func upsertCommitStatusIndex(ctx context.Context, repoID int64, sha string) (err error) { - // An atomic UPSERT operation (INSERT/UPDATE) is the only operation - // that ensures that the key is actually locked. - switch { - case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL: - _, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ - "VALUES (?,?,1) ON CONFLICT (repo_id,sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1", - repoID, sha) - case setting.Database.UseMySQL: - _, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ - "VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", - repoID, sha) - case setting.Database.UseMSSQL: - // https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ - _, err = db.Exec(ctx, "MERGE `commit_status_index` WITH (HOLDLOCK) as target "+ - "USING (SELECT ? AS repo_id, ? AS sha) AS src "+ - "ON src.repo_id = target.repo_id AND src.sha = target.sha "+ - "WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+ - "WHEN NOT MATCHED THEN INSERT (repo_id, sha, max_index) "+ - "VALUES (src.repo_id, src.sha, 1);", - repoID, sha) - default: - return fmt.Errorf("database type not supported") +func postgresGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { + res, err := db.GetEngine(ctx).Query("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ + "VALUES (?,?,1) ON CONFLICT (repo_id, sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1 RETURNING max_index", + repoID, sha) + if err != nil { + return 0, err } - return err + if len(res) == 0 { + return 0, db.ErrGetResourceIndexFailed + } + return strconv.ParseInt(string(res[0]["max_index"]), 10, 64) } // GetNextCommitStatusIndex retried 3 times to generate a resource index -func GetNextCommitStatusIndex(repoID int64, sha string) (int64, error) { - for i := 0; i < db.MaxDupIndexAttempts; i++ { - idx, err := getNextCommitStatusIndex(repoID, sha) - if err == db.ErrResouceOutdated { - continue - } +func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { + if setting.Database.UsePostgreSQL { + return postgresGetCommitStatusIndex(ctx, repoID, sha) + } + + e := db.GetEngine(ctx) + + // try to update the max_index to next value, and acquire the write-lock for the record + res, err := e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha) + if err != nil { + return 0, err + } + affected, err := res.RowsAffected() + if err != nil { + return 0, err + } + if affected == 0 { + // this slow path is only for the first time of creating a resource index + _, errIns := e.Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) VALUES (?, ?, 0)", repoID, sha) + res, err = e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha) if err != nil { return 0, err } - return idx, nil - } - return 0, db.ErrGetResourceIndexFailed -} -// getNextCommitStatusIndex return the next index -func getNextCommitStatusIndex(repoID int64, sha string) (int64, error) { - ctx, commiter, err := db.TxContext() - if err != nil { - return 0, err - } - defer commiter.Close() - - var preIdx int64 - _, err = db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", repoID, sha).Get(&preIdx) - if err != nil { - return 0, err + affected, err = res.RowsAffected() + if err != nil { + return 0, err + } + // if the update still can not update any records, the record must not exist and there must be some errors (insert error) + if affected == 0 { + if errIns == nil { + return 0, errors.New("impossible error when GetNextCommitStatusIndex, insert and update both succeeded but no record is updated") + } + return 0, errIns + } } - if err := upsertCommitStatusIndex(ctx, repoID, sha); err != nil { - return 0, err - } - - var curIdx int64 - has, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ? AND max_index=?", repoID, sha, preIdx+1).Get(&curIdx) + // now, the new index is in database (protected by the transaction and write-lock) + var newIdx int64 + has, err := e.SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id=? AND sha=?", repoID, sha).Get(&newIdx) if err != nil { return 0, err } if !has { - return 0, db.ErrResouceOutdated + return 0, errors.New("impossible error when GetNextCommitStatusIndex, upsert succeeded but no record can be selected") } - if err := commiter.Commit(); err != nil { - return 0, err - } - return curIdx, nil + return newIdx, nil } func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) { @@ -291,10 +281,8 @@ func NewCommitStatus(opts NewCommitStatusOptions) error { return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA) } - // Get the next Status Index - idx, err := GetNextCommitStatusIndex(opts.Repo.ID, opts.SHA) - if err != nil { - return fmt.Errorf("generate commit status index failed: %w", err) + if _, err := git.NewIDFromString(opts.SHA); err != nil { + return fmt.Errorf("NewCommitStatus[%s, %s]: invalid sha: %w", repoPath, opts.SHA, err) } ctx, committer, err := db.TxContext() @@ -303,6 +291,12 @@ func NewCommitStatus(opts NewCommitStatusOptions) error { } defer committer.Close() + // Get the next Status Index + idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA) + if err != nil { + return fmt.Errorf("generate commit status index failed: %w", err) + } + opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description) opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context) opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL) @@ -316,7 +310,7 @@ func NewCommitStatus(opts NewCommitStatusOptions) error { // Insert new CommitStatus if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil { - return fmt.Errorf("Insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err) + return fmt.Errorf("insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err) } return committer.Commit() diff --git a/models/issues/comment.go b/models/issues/comment.go index 6877991a93..291714799b 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -1549,3 +1549,8 @@ func FixCommentTypeLabelWithOutsideLabels() (int64, error) { return res.RowsAffected() } + +// HasOriginalAuthor returns if a comment was migrated and has an original author. +func (c *Comment) HasOriginalAuthor() bool { + return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 +} diff --git a/models/issues/issue.go b/models/issues/issue.go index ca48f425f2..3e2620691a 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -1010,12 +1010,7 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue } } - if opts.IsPull { - _, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID) - } else { - _, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID) - } - if err != nil { + if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil { return err } @@ -2471,3 +2466,8 @@ func DeleteOrphanedIssues() error { } return nil } + +// HasOriginalAuthor returns if an issue was migrated and has an original author. +func (issue *Issue) HasOriginalAuthor() bool { + return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0 +} diff --git a/models/issues/review.go b/models/issues/review.go index 3d2fceda2d..40781befe4 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -742,17 +742,9 @@ func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Commen if err != nil { return nil, err } else if official { - // recalculate the latest official review for reviewer - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) - if err != nil && !IsErrReviewNotExist(err) { + if err := restoreLatestOfficialReview(ctx, issue.ID, reviewer.ID); err != nil { return nil, err } - - if review != nil { - if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { - return nil, err - } - } } comment, err := CreateCommentCtx(ctx, &CreateCommentOptions{ @@ -770,6 +762,22 @@ func RemoveReviewRequest(issue *Issue, reviewer, doer *user_model.User) (*Commen return comment, committer.Commit() } +// Recalculate the latest official review for reviewer +func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) error { + review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID) + if err != nil && !IsErrReviewNotExist(err) { + return err + } + + if review != nil { + if _, err := db.Exec(ctx, "UPDATE `review` SET official=? WHERE id=?", true, review.ID); err != nil { + return err + } + } + + return nil +} + // AddTeamReviewRequest add a review request from one team func AddTeamReviewRequest(issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { ctx, committer, err := db.TxContext() @@ -988,6 +996,12 @@ func DeleteReview(r *Review) error { return err } + if r.Official { + if err := restoreLatestOfficialReview(ctx, r.IssueID, r.ReviewerID); err != nil { + return err + } + } + return committer.Commit() } diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 46d1cc777b..e1f3b1a803 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -201,3 +201,38 @@ func TestDismissReview(t *testing.T) { assert.False(t, requestReviewExample.Dismissed) assert.True(t, approveReviewExample.Dismissed) } + +func TestDeleteReview(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + review1, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Content: "Official rejection", + Type: issues_model.ReviewTypeReject, + Official: false, + Issue: issue, + Reviewer: user, + }) + assert.NoError(t, err) + + review2, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Content: "Official approval", + Type: issues_model.ReviewTypeApprove, + Official: true, + Issue: issue, + Reviewer: user, + }) + assert.NoError(t, err) + + assert.NoError(t, issues_model.DeleteReview(review2)) + + _, err = issues_model.GetReviewByID(db.DefaultContext, review2.ID) + assert.Error(t, err) + assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist") + + review1, err = issues_model.GetReviewByID(db.DefaultContext, review1.ID) + assert.NoError(t, err) + assert.True(t, review1.Official) +} diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index ca21eb5149..f2412881ec 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -6,6 +6,7 @@ package issues import ( "context" + "errors" "time" "code.gitea.io/gitea/models/db" @@ -47,33 +48,42 @@ func (t *TrackedTime) LoadAttributes() (err error) { } func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) { + // Load the issue if t.Issue == nil { t.Issue, err = GetIssueByID(ctx, t.IssueID) - if err != nil { - return - } - err = t.Issue.LoadRepo(ctx) - if err != nil { - return - } - } - if t.User == nil { - t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID) - if err != nil { - return - } - } - return err -} -// LoadAttributes load Issue, User -func (tl TrackedTimeList) LoadAttributes() (err error) { - for _, t := range tl { - if err = t.LoadAttributes(); err != nil { + if err != nil && !errors.Is(err, util.ErrNotExist) { return err } } - return err + // Now load the repo for the issue (which we may have just loaded) + if t.Issue != nil { + err = t.Issue.LoadRepo(ctx) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return err + } + } + // Load the user + if t.User == nil { + t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID) + if err != nil { + if !errors.Is(err, util.ErrNotExist) { + return err + } + t.User = user_model.NewGhostUser() + } + } + return nil +} + +// LoadAttributes load Issue, User +func (tl TrackedTimeList) LoadAttributes() error { + for _, t := range tl { + if err := t.LoadAttributes(); err != nil { + return err + } + } + return nil } // FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored. diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f1f943a2c2..2e661531b6 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -14,6 +14,7 @@ import ( "regexp" "strings" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -513,6 +514,13 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t return nil } + // Some migration tasks depend on the git command + if git.DefaultContext == nil { + if err = git.InitSimple(context.Background()); err != nil { + return err + } + } + // Migrate for i, m := range migrations[v-minDBVersion:] { log.Info("Migration[%d]: %s", v+int64(i), m.Description()) diff --git a/models/organization/org.go b/models/organization/org.go index 993ca3f10d..559967ef8c 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -396,13 +396,14 @@ func (org *Organization) GetOrgUserMaxAuthorizeLevel(uid int64) (perm.AccessMode } // GetUsersWhoCanCreateOrgRepo returns users which are able to create repo in organization -func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) ([]*user_model.User, error) { - users := make([]*user_model.User, 0, 10) +func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*user_model.User, error) { + // Use a map, in order to de-duplicate users. + users := make(map[int64]*user_model.User) return users, db.GetEngine(ctx). Join("INNER", "`team_user`", "`team_user`.uid=`user`.id"). Join("INNER", "`team`", "`team`.id=`team_user`.team_id"). Where(builder.Eq{"team.can_create_org_repo": true}.Or(builder.Eq{"team.authorize": perm.AccessModeOwner})). - And("team_user.org_id = ?", orgID).Asc("`user`.name").Find(&users) + And("team_user.org_id = ?", orgID).Find(&users) } // SearchOrganizationsOptions options to filter organizations @@ -458,8 +459,9 @@ func CountOrgs(opts FindOrgOptions) (int64, error) { // HasOrgOrUserVisible tells if the given user can see the given org or user func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool { - // Not SignedUser - if user == nil { + // If user is nil, it's an anonymous user/request. + // The Ghost user is handled like an anonymous user. + if user == nil || user.IsGhost() { return orgOrUser.Visibility == structs.VisibleTypePublic } diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 0fba6e2592..3bc8be44c4 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -92,11 +92,12 @@ func TestUser_GetTeams(t *testing.T) { org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) teams, err := org.LoadTeams() assert.NoError(t, err) - if assert.Len(t, teams, 4) { + if assert.Len(t, teams, 5) { assert.Equal(t, int64(1), teams[0].ID) assert.Equal(t, int64(2), teams[1].ID) assert.Equal(t, int64(12), teams[2].ID) - assert.Equal(t, int64(7), teams[3].ID) + assert.Equal(t, int64(14), teams[3].ID) + assert.Equal(t, int64(7), teams[4].ID) } } @@ -293,7 +294,7 @@ func TestUser_GetUserTeamIDs(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, teamIDs) } - testSuccess(2, []int64{1, 2}) + testSuccess(2, []int64{1, 2, 14}) testSuccess(4, []int64{2}) testSuccess(unittest.NonexistentID, []int64{}) } @@ -448,7 +449,7 @@ func TestGetUsersWhoCanCreateOrgRepo(t *testing.T) { users, err = organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, 7) assert.NoError(t, err) assert.Len(t, users, 1) - assert.EqualValues(t, 5, users[0].ID) + assert.NotNil(t, users[5]) } func TestUser_RemoveOrgRepo(t *testing.T) { diff --git a/models/packages/container/search.go b/models/packages/container/search.go index e4a5a53848..19661d44e2 100644 --- a/models/packages/container/search.go +++ b/models/packages/container/search.go @@ -26,6 +26,7 @@ type BlobSearchOptions struct { Digest string Tag string IsManifest bool + Repository string } func (opts *BlobSearchOptions) toConds() builder.Cond { @@ -54,6 +55,15 @@ func (opts *BlobSearchOptions) toConds() builder.Cond { cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property"))) } + if opts.Repository != "" { + var propsCond builder.Cond = builder.Eq{ + "package_property.ref_type": packages.PropertyTypePackage, + "package_property.name": container_module.PropertyRepository, + "package_property.value": opts.Repository, + } + + cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(propsCond).From("package_property"))) + } return cond } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index f9965bcb74..4b2a6f84c3 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -305,7 +305,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P sess := db.GetEngine(ctx). Table("package_version"). - Join("LEFT", "package_version pv2", "package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))"). + Join("LEFT", "package_version pv2", "package_version.package_id = pv2.package_id AND pv2.is_internal = ? AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))", false). Join("INNER", "package", "package.id = package_version.package_id"). Where(cond) diff --git a/models/repo.go b/models/repo.go index 569dafee5f..26fe51b5a6 100644 --- a/models/repo.go +++ b/models/repo.go @@ -444,7 +444,7 @@ func CheckRepoStats(ctx context.Context) error { }, // Repository.NumIssues { - statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, false), + statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", false), repoStatsCorrectNumIssues, "repository count 'num_issues'", }, @@ -456,7 +456,7 @@ func CheckRepoStats(ctx context.Context) error { }, // Repository.NumPulls { - statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", false, true), + statsQuery("SELECT repo.id FROM `repository` repo WHERE repo.num_pulls!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_pull=?)", true), repoStatsCorrectNumPulls, "repository count 'num_pulls'", }, diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 191970d275..fd6b61b9c3 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -498,8 +499,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { // Only show a repo that either has a topic or description. subQueryCond := builder.NewCond() - // Topic checking. Topics is non-null. - subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"})) + // Topic checking. Topics are present. + if setting.Database.UsePostgreSQL { // postgres stores the topics as json and not as text + subQueryCond = subQueryCond.Or(builder.And(builder.NotNull{"topics"}, builder.Neq{"(topics)::text": "[]"})) + } else { + subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"})) + } // Description checking. Description not empty. subQueryCond = subQueryCond.Or(builder.Neq{"description": ""}) diff --git a/models/repo/update.go b/models/repo/update.go index cc21deb0bc..3d538a4454 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -185,7 +185,7 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s return committer.Commit() } -// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize +// UpdateRepoSize updates the repository size, calculating it using getDirectorySize func UpdateRepoSize(ctx context.Context, repoID, size int64) error { _, err := db.GetEngine(ctx).ID(repoID).Cols("size").NoAutoTime().Update(&Repository{ Size: size, diff --git a/models/system/setting.go b/models/system/setting.go index 9711d38f3b..1159a0066c 100644 --- a/models/system/setting.go +++ b/models/system/setting.go @@ -12,7 +12,8 @@ import ( "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/cache" + setting_module "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "strk.kbt.io/projects/go/libravatar" @@ -35,6 +36,10 @@ func (s *Setting) TableName() string { } func (s *Setting) GetValueBool() bool { + if s == nil { + return false + } + b, _ := strconv.ParseBool(s.SettingValue) return b } @@ -75,8 +80,8 @@ func IsErrDataExpired(err error) bool { return ok } -// GetSetting returns specific setting -func GetSetting(key string) (*Setting, error) { +// GetSettingNoCache returns specific setting without using the cache +func GetSettingNoCache(key string) (*Setting, error) { v, err := GetSettings([]string{key}) if err != nil { return nil, err @@ -84,7 +89,26 @@ func GetSetting(key string) (*Setting, error) { if len(v) == 0 { return nil, ErrSettingIsNotExist{key} } - return v[key], nil + return v[strings.ToLower(key)], nil +} + +// GetSetting returns the setting value via the key +func GetSetting(key string) (string, error) { + return cache.GetString(genSettingCacheKey(key), func() (string, error) { + res, err := GetSettingNoCache(key) + if err != nil { + return "", err + } + return res.SettingValue, nil + }) +} + +// GetSettingBool return bool value of setting, +// none existing keys and errors are ignored and result in false +func GetSettingBool(key string) bool { + s, _ := GetSetting(key) + v, _ := strconv.ParseBool(s) + return v } // GetSettings returns specific settings @@ -108,7 +132,7 @@ func GetSettings(keys []string) (map[string]*Setting, error) { type AllSettings map[string]*Setting func (settings AllSettings) Get(key string) Setting { - if v, ok := settings[key]; ok { + if v, ok := settings[strings.ToLower(key)]; ok { return *v } return Setting{} @@ -139,12 +163,13 @@ func GetAllSettings() (AllSettings, error) { // DeleteSetting deletes a specific setting for a user func DeleteSetting(setting *Setting) error { + cache.Remove(genSettingCacheKey(setting.SettingKey)) _, err := db.GetEngine(db.DefaultContext).Delete(setting) return err } func SetSettingNoVersion(key, value string) error { - s, err := GetSetting(key) + s, err := GetSettingNoCache(key) if IsErrSettingIsNotExist(err) { return SetSetting(&Setting{ SettingKey: key, @@ -163,7 +188,14 @@ func SetSetting(setting *Setting) error { if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil { return err } + setting.Version++ + + cc := cache.GetCache() + if cc != nil { + return cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds()) + } + return nil } @@ -213,9 +245,9 @@ var ( func Init() error { var disableGravatar bool - disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar) + disableGravatarSetting, err := GetSettingNoCache(KeyPictureDisableGravatar) if IsErrSettingIsNotExist(err) { - disableGravatar = setting.GetDefaultDisableGravatar() + disableGravatar = setting_module.GetDefaultDisableGravatar() disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)} } else if err != nil { return err @@ -224,9 +256,9 @@ func Init() error { } var enableFederatedAvatar bool - enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar) + enableFederatedAvatarSetting, err := GetSettingNoCache(KeyPictureEnableFederatedAvatar) if IsErrSettingIsNotExist(err) { - enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar) + enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar) enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)} } else if err != nil { return err @@ -234,20 +266,30 @@ func Init() error { enableFederatedAvatar = disableGravatarSetting.GetValueBool() } - if setting.OfflineMode { + if setting_module.OfflineMode { disableGravatar = true enableFederatedAvatar = false - } - - if disableGravatar || !enableFederatedAvatar { - var err error - GravatarSourceURL, err = url.Parse(setting.GravatarSource) - if err != nil { - return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting.GravatarSource, err) + if !GetSettingBool(KeyPictureDisableGravatar) { + if err := SetSettingNoVersion(KeyPictureDisableGravatar, "true"); err != nil { + return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err) + } + } + if GetSettingBool(KeyPictureEnableFederatedAvatar) { + if err := SetSettingNoVersion(KeyPictureEnableFederatedAvatar, "false"); err != nil { + return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err) + } } } - if enableFederatedAvatarSetting.GetValueBool() { + if enableFederatedAvatar || !disableGravatar { + var err error + GravatarSourceURL, err = url.Parse(setting_module.GravatarSource) + if err != nil { + return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err) + } + } + + if GravatarSourceURL != nil && enableFederatedAvatarSetting.GetValueBool() { LibravatarService = libravatar.New() if GravatarSourceURL.Scheme == "https" { LibravatarService.SetUseHTTPS(true) diff --git a/models/system/setting_key.go b/models/system/setting_key.go index 5a6ea6ed72..14105b89d0 100644 --- a/models/system/setting_key.go +++ b/models/system/setting_key.go @@ -9,3 +9,8 @@ const ( KeyPictureDisableGravatar = "picture.disable_gravatar" KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar" ) + +// genSettingCacheKey returns the cache key for some configuration +func genSettingCacheKey(key string) string { + return "system.setting." + key +} diff --git a/models/system/setting_test.go b/models/system/setting_test.go index d25fc05f31..ecfefeb097 100644 --- a/models/system/setting_test.go +++ b/models/system/setting_test.go @@ -34,10 +34,14 @@ func TestSettings(t *testing.T) { assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue) // updated setting - updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: newSetting.Version} + updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: settings[strings.ToLower(keyName)].Version} err = system.SetSetting(updatedSetting) assert.NoError(t, err) + value, err := system.GetSetting(keyName) + assert.NoError(t, err) + assert.EqualValues(t, updatedSetting.SettingValue, value) + // get all settings settings, err = system.GetAllSettings() assert.NoError(t, err) diff --git a/models/user.go b/models/user.go index 85f465127a..0fc28ff055 100644 --- a/models/user.go +++ b/models/user.go @@ -89,6 +89,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) &user_model.UserBadge{UserID: u.ID}, &pull_model.AutoMerge{DoerID: u.ID}, &pull_model.ReviewState{UserID: u.ID}, + &user_model.Redirect{RedirectUserID: u.ID}, ); err != nil { return fmt.Errorf("deleteBeans: %w", err) } diff --git a/models/user/avatar.go b/models/user/avatar.go index f73ac56c5e..08b3b0e331 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -68,11 +68,7 @@ func (u *User) AvatarLinkWithSize(size int) string { useLocalAvatar := false autoGenerateAvatar := false - var disableGravatar bool - disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar) - if disableGravatarSetting != nil { - disableGravatar = disableGravatarSetting.GetValueBool() - } + disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar) switch { case u.UseCustomAvatar: diff --git a/models/user/must_change_password.go b/models/user/must_change_password.go new file mode 100644 index 0000000000..826b2fac6f --- /dev/null +++ b/models/user/must_change_password.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + + "xorm.io/builder" +) + +func SetMustChangePassword(ctx context.Context, all, mustChangePassword bool, include, exclude []string) (int64, error) { + sliceTrimSpaceDropEmpty := func(input []string) []string { + output := make([]string, 0, len(input)) + for _, in := range input { + in = strings.ToLower(strings.TrimSpace(in)) + if in == "" { + continue + } + output = append(output, in) + } + return output + } + + var cond builder.Cond + + // Only include the users where something changes to get an accurate count + cond = builder.Neq{"must_change_password": mustChangePassword} + + if !all { + include = sliceTrimSpaceDropEmpty(include) + if len(include) == 0 { + return 0, fmt.Errorf("no users to include provided") + } + + cond = cond.And(builder.In("lower_name", include)) + } + + exclude = sliceTrimSpaceDropEmpty(exclude) + if len(exclude) > 0 { + cond = cond.And(builder.NotIn("lower_name", exclude)) + } + + return db.GetEngine(ctx).Where(cond).MustCols("must_change_password").Update(&User{MustChangePassword: mustChangePassword}) +} diff --git a/models/user/setting.go b/models/user/setting.go index 5fe7c2ec23..c88ec51dbd 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/cache" "xorm.io/builder" ) @@ -47,9 +48,25 @@ func IsErrUserSettingIsNotExist(err error) bool { return ok } -// GetSetting returns specific setting -func GetSetting(uid int64, key string) (*Setting, error) { - v, err := GetUserSettings(uid, []string{key}) +// genSettingCacheKey returns the cache key for some configuration +func genSettingCacheKey(userID int64, key string) string { + return fmt.Sprintf("user_%d.setting.%s", userID, key) +} + +// GetSetting returns the setting value via the key +func GetSetting(uid int64, key string) (string, error) { + return cache.GetString(genSettingCacheKey(uid, key), func() (string, error) { + res, err := GetSettingNoCache(uid, key) + if err != nil { + return "", err + } + return res.SettingValue, nil + }) +} + +// GetSettingNoCache returns specific setting without using the cache +func GetSettingNoCache(uid int64, key string) (*Setting, error) { + v, err := GetSettings(uid, []string{key}) if err != nil { return nil, err } @@ -59,8 +76,8 @@ func GetSetting(uid int64, key string) (*Setting, error) { return v[key], nil } -// GetUserSettings returns specific settings from user -func GetUserSettings(uid int64, keys []string) (map[string]*Setting, error) { +// GetSettings returns specific settings from user +func GetSettings(uid int64, keys []string) (map[string]*Setting, error) { settings := make([]*Setting, 0, len(keys)) if err := db.GetEngine(db.DefaultContext). Where("user_id=?", uid). @@ -105,6 +122,7 @@ func GetUserSetting(userID int64, key string, def ...string) (string, error) { if err := validateUserSettingKey(key); err != nil { return "", err } + setting := &Setting{UserID: userID, SettingKey: key} has, err := db.GetEngine(db.DefaultContext).Get(setting) if err != nil { @@ -124,7 +142,10 @@ func DeleteUserSetting(userID int64, key string) error { if err := validateUserSettingKey(key); err != nil { return err } + + cache.Remove(genSettingCacheKey(userID, key)) _, err := db.GetEngine(db.DefaultContext).Delete(&Setting{UserID: userID, SettingKey: key}) + return err } @@ -133,7 +154,12 @@ func SetUserSetting(userID int64, key, value string) error { if err := validateUserSettingKey(key); err != nil { return err } - return upsertUserSettingValue(userID, key, value) + + _, err := cache.GetString(genSettingCacheKey(userID, key), func() (string, error) { + return value, upsertUserSettingValue(userID, key, value) + }) + + return err } func upsertUserSettingValue(userID int64, key, value string) error { diff --git a/models/user/setting_test.go b/models/user/setting_test.go index f0083038df..5a772a8ce7 100644 --- a/models/user/setting_test.go +++ b/models/user/setting_test.go @@ -27,7 +27,7 @@ func TestSettings(t *testing.T) { assert.NoError(t, err) // get specific setting - settings, err := user_model.GetUserSettings(99, []string{keyName}) + settings, err := user_model.GetSettings(99, []string{keyName}) assert.NoError(t, err) assert.Len(t, settings, 1) assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue) diff --git a/models/user/user.go b/models/user/user.go index 9a2da6dbc1..7aa2635624 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -7,8 +7,6 @@ package user import ( "context" - "crypto/sha256" - "crypto/subtle" "encoding/hex" "fmt" "net/url" @@ -22,6 +20,7 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/auth/openid" + "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -30,10 +29,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "golang.org/x/crypto/argon2" - "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/pbkdf2" - "golang.org/x/crypto/scrypt" "xorm.io/builder" ) @@ -48,21 +43,6 @@ const ( UserTypeOrganization ) -const ( - algoBcrypt = "bcrypt" - algoScrypt = "scrypt" - algoArgon2 = "argon2" - algoPbkdf2 = "pbkdf2" -) - -// AvailableHashAlgorithms represents the available password hashing algorithms -var AvailableHashAlgorithms = []string{ - algoPbkdf2, - algoArgon2, - algoScrypt, - algoBcrypt, -} - const ( // EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own EmailNotificationsEnabled = "enabled" @@ -368,42 +348,6 @@ func (u *User) NewGitSig() *git.Signature { } } -func hashPassword(passwd, salt, algo string) (string, error) { - var tempPasswd []byte - var saltBytes []byte - - // There are two formats for the Salt value: - // * The new format is a (32+)-byte hex-encoded string - // * The old format was a 10-byte binary format - // We have to tolerate both here but Authenticate should - // regenerate the Salt following a successful validation. - if len(salt) == 10 { - saltBytes = []byte(salt) - } else { - var err error - saltBytes, err = hex.DecodeString(salt) - if err != nil { - return "", err - } - } - - switch algo { - case algoBcrypt: - tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost) - return string(tempPasswd), nil - case algoScrypt: - tempPasswd, _ = scrypt.Key([]byte(passwd), saltBytes, 65536, 16, 2, 50) - case algoArgon2: - tempPasswd = argon2.IDKey([]byte(passwd), saltBytes, 2, 65536, 8, 50) - case algoPbkdf2: - fallthrough - default: - tempPasswd = pbkdf2.Key([]byte(passwd), saltBytes, 10000, 50, sha256.New) - } - - return fmt.Sprintf("%x", tempPasswd), nil -} - // SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO // change passwd, salt and passwd_hash_algo fields func (u *User) SetPassword(passwd string) (err error) { @@ -417,7 +361,7 @@ func (u *User) SetPassword(passwd string) (err error) { if u.Salt, err = GetUserSalt(); err != nil { return err } - if u.Passwd, err = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo); err != nil { + if u.Passwd, err = hash.Parse(setting.PasswordHashAlgo).Hash(passwd, u.Salt); err != nil { return err } u.PasswdHashAlgo = setting.PasswordHashAlgo @@ -425,20 +369,9 @@ func (u *User) SetPassword(passwd string) (err error) { return nil } -// ValidatePassword checks if given password matches the one belongs to the user. +// ValidatePassword checks if the given password matches the one belonging to the user. func (u *User) ValidatePassword(passwd string) bool { - tempHash, err := hashPassword(passwd, u.Salt, u.PasswdHashAlgo) - if err != nil { - return false - } - - if u.PasswdHashAlgo != algoBcrypt && subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1 { - return true - } - if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil { - return true - } - return false + return hash.Parse(u.PasswdHashAlgo).VerifyPassword(passwd, u.Passwd, u.Salt) } // IsPasswordSet checks if the password is set or left empty @@ -1227,7 +1160,10 @@ func GetUserByOpenID(uri string) (*User, error) { // GetAdminUser returns the first administrator func GetAdminUser() (*User, error) { var admin User - has, err := db.GetEngine(db.DefaultContext).Where("is_admin=?", true).Get(&admin) + has, err := db.GetEngine(db.DefaultContext). + Where("is_admin=?", true). + Asc("id"). // Reliably get the admin with the lowest ID. + Get(&admin) if err != nil { return nil, err } else if !has { diff --git a/models/user/user_test.go b/models/user/user_test.go index 5f2ac0a60c..37fa711d83 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -162,7 +163,7 @@ func TestEmailNotificationPreferences(t *testing.T) { func TestHashPasswordDeterministic(t *testing.T) { b := make([]byte, 16) u := &user_model.User{} - algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"} + algos := hash.RecommendedHashAlgorithms for j := 0; j < len(algos); j++ { u.PasswdHashAlgo = algos[j] for i := 0; i < 50; i++ { diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index aebe0d6e72..108330bca2 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -155,6 +155,7 @@ type HookType = string // Types of webhooks const ( + FORGEJO HookType = "forgejo" GITEA HookType = "gitea" GOGS HookType = "gogs" SLACK HookType = "slack" diff --git a/modules/activitypub/user_settings.go b/modules/activitypub/user_settings.go index fc9775b0f0..d192b9cdb2 100644 --- a/modules/activitypub/user_settings.go +++ b/modules/activitypub/user_settings.go @@ -11,7 +11,7 @@ import ( // GetKeyPair function returns a user's private and public keys func GetKeyPair(user *user_model.User) (pub, priv string, err error) { var settings map[string]*user_model.Setting - settings, err = user_model.GetUserSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem}) + settings, err = user_model.GetSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem}) if err != nil { return } else if len(settings) == 0 { diff --git a/modules/auth/password/hash/argon2.go b/modules/auth/password/hash/argon2.go new file mode 100644 index 0000000000..9216258044 --- /dev/null +++ b/modules/auth/password/hash/argon2.go @@ -0,0 +1,77 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package hash + +import ( + "encoding/hex" + "strings" + + "code.gitea.io/gitea/modules/log" + + "golang.org/x/crypto/argon2" +) + +func init() { + Register("argon2", NewArgon2Hasher) +} + +// Argon2Hasher implements PasswordHasher +// and uses the Argon2 key derivation function, hybrant variant +type Argon2Hasher struct { + time uint32 + memory uint32 + threads uint8 + keyLen uint32 +} + +// HashWithSaltBytes a provided password and salt +func (hasher *Argon2Hasher) HashWithSaltBytes(password string, salt []byte) string { + if hasher == nil { + return "" + } + return hex.EncodeToString(argon2.IDKey([]byte(password), salt, hasher.time, hasher.memory, hasher.threads, hasher.keyLen)) +} + +// NewArgon2Hasher is a factory method to create an Argon2Hasher +// The provided config should be either empty or of the form: +// "

$", where is the string representation +// of an integer +func NewScryptHasher(config string) *ScryptHasher { + hasher := &ScryptHasher{ + n: 1 << 16, + r: 16, + p: 2, // 2 passes through memory - this default config will use 128MiB in total. + keyLen: 50, + } + + if config == "" { + return hasher + } + + vals := strings.SplitN(config, "$", 4) + if len(vals) != 4 { + log.Error("invalid scrypt hash spec %s", config) + return nil + } + var err error + hasher.n, err = parseIntParam(vals[0], "n", "scrypt", config, nil) + hasher.r, err = parseIntParam(vals[1], "r", "scrypt", config, err) + hasher.p, err = parseIntParam(vals[2], "p", "scrypt", config, err) + hasher.keyLen, err = parseIntParam(vals[3], "keyLen", "scrypt", config, err) + if err != nil { + return nil + } + return hasher +} diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go new file mode 100644 index 0000000000..f33c3ba376 --- /dev/null +++ b/modules/auth/password/hash/setting.go @@ -0,0 +1,51 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package hash + +// DefaultHashAlgorithmName represents the default value of PASSWORD_HASH_ALGO +// configured in app.ini. +// +// It is NOT the same and does NOT map to the defaultEmptyHashAlgorithmSpecification. +// +// It will be dealiased as per aliasAlgorithmNames whereas +// defaultEmptyHashAlgorithmSpecification does not undergo dealiasing. +const DefaultHashAlgorithmName = "pbkdf2_hi" + +var DefaultHashAlgorithm *PasswordHashAlgorithm + +var aliasAlgorithmNames = map[string]string{ + "argon2": "argon2$2$65536$8$50", + "bcrypt": "bcrypt$10", + "scrypt": "scrypt$65536$16$2$50", + "pbkdf2": "pbkdf2_v2", // pbkdf2 should default to pbkdf2_v2 + "pbkdf2_v1": "pbkdf2$10000$50", + // The latest PBKDF2 password algorithm is used as the default since it doesn't + // use a lot of memory and is safer to use on less powerful devices. + "pbkdf2_v2": "pbkdf2$50000$50", + // The pbkdf2_hi password algorithm is offered as a stronger alternative to the + // slightly improved pbkdf2_v2 algorithm + "pbkdf2_hi": "pbkdf2$320000$50", +} + +var RecommendedHashAlgorithms = []string{ + "pbkdf2", + "argon2", + "bcrypt", + "scrypt", + "pbkdf2_hi", +} + +func SetDefaultPasswordHashAlgorithm(algorithmName string) (string, *PasswordHashAlgorithm) { + if algorithmName == "" { + algorithmName = DefaultHashAlgorithmName + } + alias, has := aliasAlgorithmNames[algorithmName] + for has { + algorithmName = alias + alias, has = aliasAlgorithmNames[algorithmName] + } + DefaultHashAlgorithm = Parse(algorithmName) + + return algorithmName, DefaultHashAlgorithm +} diff --git a/modules/auth/password/hash/setting_test.go b/modules/auth/password/hash/setting_test.go new file mode 100644 index 0000000000..04965363a1 --- /dev/null +++ b/modules/auth/password/hash/setting_test.go @@ -0,0 +1,38 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package hash + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckSettingPasswordHashAlgorithm(t *testing.T) { + t.Run("pbkdf2 is pbkdf2_v2", func(t *testing.T) { + pbkdf2v2Config, pbkdf2v2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2_v2") + pbkdf2Config, pbkdf2Algo := SetDefaultPasswordHashAlgorithm("pbkdf2") + + assert.Equal(t, pbkdf2v2Config, pbkdf2Config) + assert.Equal(t, pbkdf2v2Algo.Name, pbkdf2Algo.Name) + }) + + for a, b := range aliasAlgorithmNames { + t.Run(a+"="+b, func(t *testing.T) { + aConfig, aAlgo := SetDefaultPasswordHashAlgorithm(a) + bConfig, bAlgo := SetDefaultPasswordHashAlgorithm(b) + + assert.Equal(t, bConfig, aConfig) + assert.Equal(t, aAlgo.Name, bAlgo.Name) + }) + } + + t.Run("pbkdf2_hi is the default when default password hash algorithm is empty", func(t *testing.T) { + emptyConfig, emptyAlgo := SetDefaultPasswordHashAlgorithm("") + pbkdf2hiConfig, pbkdf2hiAlgo := SetDefaultPasswordHashAlgorithm("pbkdf2_hi") + + assert.Equal(t, pbkdf2hiConfig, emptyConfig) + assert.Equal(t, pbkdf2hiAlgo.Name, emptyAlgo.Name) + }) +} diff --git a/modules/password/password.go b/modules/auth/password/password.go similarity index 93% rename from modules/password/password.go rename to modules/auth/password/password.go index e1f1f769ec..2bad389f76 100644 --- a/modules/password/password.go +++ b/modules/auth/password/password.go @@ -12,8 +12,8 @@ import ( "strings" "sync" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" ) // complexity contains information about a particular kind of password complexity @@ -113,13 +113,13 @@ func Generate(n int) (string, error) { } // BuildComplexityError builds the error message when password complexity checks fail -func BuildComplexityError(ctx *context.Context) string { +func BuildComplexityError(locale translation.Locale) string { var buffer bytes.Buffer - buffer.WriteString(ctx.Tr("form.password_complexity")) + buffer.WriteString(locale.Tr("form.password_complexity")) buffer.WriteString("

    ") for _, c := range requiredList { buffer.WriteString("
  • ") - buffer.WriteString(ctx.Tr(c.TrNameOne)) + buffer.WriteString(locale.Tr(c.TrNameOne)) buffer.WriteString("
  • ") } buffer.WriteString("
") diff --git a/modules/password/password_test.go b/modules/auth/password/password_test.go similarity index 100% rename from modules/password/password_test.go rename to modules/auth/password/password_test.go diff --git a/modules/password/pwn.go b/modules/auth/password/pwn.go similarity index 100% rename from modules/password/pwn.go rename to modules/auth/password/pwn.go diff --git a/modules/cache/cache.go b/modules/cache/cache.go index 21d0cd0a04..474ede3cf3 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -51,27 +51,26 @@ func GetString(key string, getFunc func() (string, error)) (string, error) { if conn == nil || setting.CacheService.TTL == 0 { return getFunc() } - if !conn.IsExist(key) { - var ( - value string - err error - ) - if value, err = getFunc(); err != nil { + + cached := conn.Get(key) + + if cached == nil { + value, err := getFunc() + if err != nil { return value, err } - err = conn.Put(key, value, setting.CacheService.TTLSeconds()) - if err != nil { - return "", err - } + return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) } - value := conn.Get(key) - if v, ok := value.(string); ok { - return v, nil + + if value, ok := cached.(string); ok { + return value, nil } - if v, ok := value.(fmt.Stringer); ok { - return v.String(), nil + + if stringer, ok := cached.(fmt.Stringer); ok { + return stringer.String(), nil } - return fmt.Sprintf("%s", conn.Get(key)), nil + + return fmt.Sprintf("%s", cached), nil } // GetInt returns key value from cache with callback when no key exists in cache @@ -79,30 +78,33 @@ func GetInt(key string, getFunc func() (int, error)) (int, error) { if conn == nil || setting.CacheService.TTL == 0 { return getFunc() } - if !conn.IsExist(key) { - var ( - value int - err error - ) - if value, err = getFunc(); err != nil { + + cached := conn.Get(key) + + if cached == nil { + value, err := getFunc() + if err != nil { return value, err } - err = conn.Put(key, value, setting.CacheService.TTLSeconds()) - if err != nil { - return 0, err - } + + return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) } - switch value := conn.Get(key).(type) { + + switch v := cached.(type) { case int: - return value, nil + return v, nil case string: - v, err := strconv.Atoi(value) + value, err := strconv.Atoi(v) if err != nil { return 0, err } - return v, nil + return value, nil default: - return 0, fmt.Errorf("Unsupported cached value type: %v", value) + value, err := getFunc() + if err != nil { + return value, err + } + return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) } } @@ -111,30 +113,34 @@ func GetInt64(key string, getFunc func() (int64, error)) (int64, error) { if conn == nil || setting.CacheService.TTL == 0 { return getFunc() } - if !conn.IsExist(key) { - var ( - value int64 - err error - ) - if value, err = getFunc(); err != nil { + + cached := conn.Get(key) + + if cached == nil { + value, err := getFunc() + if err != nil { return value, err } - err = conn.Put(key, value, setting.CacheService.TTLSeconds()) - if err != nil { - return 0, err - } + + return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) } - switch value := conn.Get(key).(type) { + + switch v := conn.Get(key).(type) { case int64: - return value, nil + return v, nil case string: - v, err := strconv.ParseInt(value, 10, 64) + value, err := strconv.ParseInt(v, 10, 64) if err != nil { return 0, err } - return v, nil + return value, nil default: - return 0, fmt.Errorf("Unsupported cached value type: %v", value) + value, err := getFunc() + if err != nil { + return value, err + } + + return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) } } diff --git a/modules/charset/ambiguous.go b/modules/charset/ambiguous.go index 9dab3b0951..ef5e7650a6 100644 --- a/modules/charset/ambiguous.go +++ b/modules/charset/ambiguous.go @@ -29,6 +29,12 @@ func AmbiguousTablesForLocale(locale translation.Locale) []*AmbiguousTable { key = key[:idx] } } + if table == nil && (locale.Language() == "zh-CN" || locale.Language() == "zh_CN") { + table = AmbiguousCharacters["zh-hans"] + } + if table == nil && strings.HasPrefix(locale.Language(), "zh") { + table = AmbiguousCharacters["zh-hant"] + } if table == nil { table = AmbiguousCharacters["_default"] } diff --git a/modules/charset/escape.go b/modules/charset/escape.go index b264a569ff..df10c8fbd0 100644 --- a/modules/charset/escape.go +++ b/modules/charset/escape.go @@ -9,6 +9,7 @@ package charset import ( + "bufio" "io" "strings" @@ -32,7 +33,7 @@ func EscapeControlHTML(text string, locale translation.Locale, allowed ...rune) return streamer.escaped, sb.String() } -// EscapeControlReaders escapes the unicode control sequences in a provider reader and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte +// EscapeControlReaders escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) { outputStream := &HTMLStreamerWriter{Writer: writer} streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer) @@ -44,6 +45,31 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation. return streamer.escaped, err } +// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte. HTML line breaks are not inserted after every newline by this method. +func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) { + bufRd := bufio.NewReader(reader) + outputStream := &HTMLStreamerWriter{Writer: writer} + streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer) + + for { + line, rdErr := bufRd.ReadString('\n') + if len(line) > 0 { + if err := streamer.Text(line); err != nil { + streamer.escaped.HasError = true + log.Error("Error whilst escaping: %v", err) + return streamer.escaped, err + } + } + if rdErr != nil { + if rdErr != io.EOF { + err = rdErr + } + break + } + } + return streamer.escaped, err +} + // EscapeControlString escapes the unicode control sequences in a provided string and returns the findings as an EscapeStatus and the escaped string func EscapeControlString(text string, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output string) { sb := &strings.Builder{} diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index e5f303d26f..2ec16e0fd5 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -7,7 +7,6 @@ package charset import ( "fmt" "regexp" - "sort" "strings" "unicode" "unicode/utf8" @@ -21,12 +20,16 @@ import ( var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`) func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer { + allowedM := make(map[rune]bool, len(allowed)) + for _, v := range allowed { + allowedM[v] = true + } return &escapeStreamer{ escaped: &EscapeStatus{}, PassthroughHTMLStreamer: *NewPassthroughStreamer(next), locale: locale, ambiguousTables: AmbiguousTablesForLocale(locale), - allowed: allowed, + allowed: allowedM, } } @@ -35,7 +38,7 @@ type escapeStreamer struct { escaped *EscapeStatus locale translation.Locale ambiguousTables []*AmbiguousTable - allowed []rune + allowed map[rune]bool } func (e *escapeStreamer) EscapeStatus() *EscapeStatus { @@ -257,7 +260,7 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables runeCounts.numBrokenRunes++ case r == ' ' || r == '\t' || r == '\n': runeCounts.numBasicRunes++ - case e.isAllowed(r): + case e.allowed[r]: if r > 0x7e || r < 0x20 { types[i] = nonBasicASCIIRuneType runeCounts.numNonConfusingNonBasicRunes++ @@ -283,16 +286,3 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables } return types, confusables, runeCounts } - -func (e *escapeStreamer) isAllowed(r rune) bool { - if len(e.allowed) == 0 { - return false - } - if len(e.allowed) == 1 { - return e.allowed[0] == r - } - - return sort.Search(len(e.allowed), func(i int) bool { - return e.allowed[i] >= r - }) >= 0 -} diff --git a/modules/context/access_log.go b/modules/context/access_log.go index 1a10c4763a..0560131a38 100644 --- a/modules/context/access_log.go +++ b/modules/context/access_log.go @@ -7,8 +7,8 @@ package context import ( "bytes" "context" - "html/template" "net/http" + "text/template" "time" "code.gitea.io/gitea/modules/log" diff --git a/modules/context/api.go b/modules/context/api.go index b9d130e2a8..d95e580321 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -220,7 +220,13 @@ func (ctx *APIContext) CheckForOTP() { func APIAuth(authMethod auth_service.Method) func(*APIContext) { return func(ctx *APIContext) { // Get user from session if logged in. - ctx.Doer = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + var err error + ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + if err != nil { + ctx.Error(http.StatusUnauthorized, "APIAuth", err) + return + } + if ctx.Doer != nil { if ctx.Locale.Language() != ctx.Doer.Language { ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) @@ -388,7 +394,7 @@ func RepoRefForAPI(next http.Handler) http.Handler { return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if len(refName) == 40 { + } else if len(refName) == git.SHAFullLength { ctx.Repo.CommitID = refName ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) if err != nil { diff --git a/modules/context/context.go b/modules/context/context.go index 4b6a21b217..b761f7b803 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -34,6 +34,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/auth" @@ -322,9 +323,9 @@ func (ctx *Context) plainTextInternal(skip, status int, bs []byte) { if statusPrefix == 4 || statusPrefix == 5 { log.Log(skip, log.TRACE, "plainTextInternal (status=%d): %s", status, string(bs)) } - ctx.Resp.WriteHeader(status) ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff") + ctx.Resp.WriteHeader(status) if _, err := ctx.Resp.Write(bs); err != nil { log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err) } @@ -345,34 +346,61 @@ func (ctx *Context) RespHeader() http.Header { return ctx.Resp.Header() } +type ServeHeaderOptions struct { + ContentType string // defaults to "application/octet-stream" + ContentTypeCharset string + ContentLength *int64 + Disposition string // defaults to "attachment" + Filename string + CacheDuration time.Duration // defaults to 5 minutes + LastModified time.Time +} + // SetServeHeaders sets necessary content serve headers -func (ctx *Context) SetServeHeaders(filename string) { - ctx.Resp.Header().Set("Content-Description", "File Transfer") - ctx.Resp.Header().Set("Content-Type", "application/octet-stream") - ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+filename) - ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") - ctx.Resp.Header().Set("Expires", "0") - ctx.Resp.Header().Set("Cache-Control", "must-revalidate") - ctx.Resp.Header().Set("Pragma", "public") - ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") +func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) { + header := ctx.Resp.Header() + + contentType := typesniffer.ApplicationOctetStream + if opts.ContentType != "" { + if opts.ContentTypeCharset != "" { + contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) + } else { + contentType = opts.ContentType + } + } + header.Set("Content-Type", contentType) + header.Set("X-Content-Type-Options", "nosniff") + + if opts.ContentLength != nil { + header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) + } + + if opts.Filename != "" { + disposition := opts.Disposition + if disposition == "" { + disposition = "attachment" + } + + backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \" + header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename))) + header.Set("Access-Control-Expose-Headers", "Content-Disposition") + } + + duration := opts.CacheDuration + if duration == 0 { + duration = 5 * time.Minute + } + httpcache.AddCacheControlToHeader(header, duration) + + if !opts.LastModified.IsZero() { + header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) + } } // ServeContent serves content to http request -func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) { - ctx.SetServeHeaders(name) - http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r) -} - -// ServeFile serves given file to response. -func (ctx *Context) ServeFile(file string, names ...string) { - var name string - if len(names) > 0 { - name = names[0] - } else { - name = path.Base(file) - } - ctx.SetServeHeaders(name) - http.ServeFile(ctx.Resp, ctx.Req, file) +func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { + ctx.SetServeHeaders(opts) + http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r) } // UploadStream returns the request body or the first form file @@ -635,7 +663,13 @@ func getCsrfOpts() CsrfOptions { // Auth converts auth.Auth as a middleware func Auth(authMethod auth.Method) func(*Context) { return func(ctx *Context) { - ctx.Doer = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + var err error + ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + if err != nil { + log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err) + ctx.Error(http.StatusUnauthorized, "Verify") + return + } if ctx.Doer != nil { if ctx.Locale.Language() != ctx.Doer.Language { ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) diff --git a/modules/context/repo.go b/modules/context/repo.go index 1a0263a330..3bf6431e2c 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -816,7 +816,7 @@ func getRefName(ctx *Context, pathType RepoRefType) string { } // For legacy and API support only full commit sha parts := strings.Split(path, "/") - if len(parts) > 0 && len(parts[0]) == 40 { + if len(parts) > 0 && len(parts[0]) == git.SHAFullLength { ctx.Repo.TreePath = strings.Join(parts[1:], "/") return parts[0] } @@ -852,7 +852,7 @@ func getRefName(ctx *Context, pathType RepoRefType) string { return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist) case RepoRefCommit: parts := strings.Split(path, "/") - if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= 40 { + if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength { ctx.Repo.TreePath = strings.Join(parts[1:], "/") return parts[0] } @@ -961,7 +961,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if len(refName) >= 7 && len(refName) <= 40 { + } else if len(refName) >= 7 && len(refName) <= git.SHAFullLength { ctx.Repo.IsViewCommit = true ctx.Repo.CommitID = refName @@ -971,7 +971,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return } // If short commit ID add canonical link header - if len(refName) < 40 { + if len(refName) < git.SHAFullLength { ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"", util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)))) } @@ -1087,6 +1087,9 @@ func (ctx *Context) IssueTemplatesErrorsFromDefaultBranch() ([]*api.IssueTemplat if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil { invalidFiles[fullName] = err } else { + if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/ + it.Ref = git.BranchPrefix + it.Ref + } issueTemplates = append(issueTemplates, it) } } diff --git a/modules/convert/issue.go b/modules/convert/issue.go index 5364367a80..feedc6f8de 100644 --- a/modules/convert/issue.go +++ b/modules/convert/issue.go @@ -110,12 +110,11 @@ func ToAPIIssueList(il issues_model.IssueList) []*api.Issue { // ToTrackedTime converts TrackedTime to API format func ToTrackedTime(t *issues_model.TrackedTime) (apiT *api.TrackedTime) { apiT = &api.TrackedTime{ - ID: t.ID, - IssueID: t.IssueID, - UserID: t.UserID, - UserName: t.User.Name, - Time: t.Time, - Created: t.Created, + ID: t.ID, + IssueID: t.IssueID, + UserID: t.UserID, + Time: t.Time, + Created: t.Created, } if t.Issue != nil { apiT.Issue = ToAPIIssue(t.Issue) diff --git a/modules/convert/pull.go b/modules/convert/pull.go index 9c31f9bd2c..5eb3cba5a3 100644 --- a/modules/convert/pull.go +++ b/modules/convert/pull.go @@ -89,6 +89,10 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u }, } + if pr.Issue.ClosedUnix != 0 { + apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr() + } + gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) if err != nil { log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err) diff --git a/modules/doctor/dbconsistency.go b/modules/doctor/dbconsistency.go index 7ae349908e..89d974a350 100644 --- a/modules/doctor/dbconsistency.go +++ b/modules/doctor/dbconsistency.go @@ -205,6 +205,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er // find stopwatches without existing issue genericOrphanCheck("Orphaned Stopwatches without existing Issue", "stopwatch", "issue", "stopwatch.issue_id=`issue`.id"), + // find redirects without existing user. + genericOrphanCheck("Orphaned Redirects without existing redirect user", + "user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"), ) for _, c := range consistencyChecks { diff --git a/modules/doctor/heads.go b/modules/doctor/heads.go index 7f3b2a8a02..b1bfd50b20 100644 --- a/modules/doctor/heads.go +++ b/modules/doctor/heads.go @@ -19,11 +19,9 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) numReposUpdated := 0 err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { numRepos++ - runOpts := &git.RunOpts{Dir: repo.RepoPath()} + _, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) - _, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(runOpts) - - head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(runOpts) + head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) // what we expect: default branch is valid, and HEAD points to it if headErr == nil && defaultBranchErr == nil && head == repo.DefaultBranch { @@ -49,7 +47,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) } // otherwise, let's try fixing HEAD - err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", repo.DefaultBranch).Run(runOpts) + err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", git.BranchPrefix+repo.DefaultBranch).Run(&git.RunOpts{Dir: repo.RepoPath()}) if err != nil { logger.Warn("Failed to fix HEAD for %s/%s: %v", repo.OwnerName, repo.Name, err) return nil @@ -65,7 +63,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) logger.Info("Out of %d repos, HEADs for %d are now fixed and HEADS for %d are still broken", numRepos, numReposUpdated, numDefaultBranchesBroken+numHeadsBroken-numReposUpdated) } else { if numHeadsBroken == 0 && numDefaultBranchesBroken == 0 { - logger.Info("All %d repos have their HEADs in the correct state") + logger.Info("All %d repos have their HEADs in the correct state", numRepos) } else { if numHeadsBroken == 0 && numDefaultBranchesBroken != 0 { logger.Critical("Default branches are broken for %d/%d repos", numDefaultBranchesBroken, numRepos) diff --git a/modules/emoji/emoji_data.go b/modules/emoji/emoji_data.go index 1e14d3de6b..cc60155045 100644 --- a/modules/emoji/emoji_data.go +++ b/modules/emoji/emoji_data.go @@ -21,6 +21,7 @@ var GemojiData = Gemoji{ {"\U0001f524", "input latin letters", []string{"abc"}, "6.0", false}, {"\U0001f521", "input latin lowercase", []string{"abcd"}, "6.0", false}, {"\U0001f251", "Japanese “acceptable” button", []string{"accept"}, "6.0", false}, + {"\U0001fa97", "accordion", []string{"accordion"}, "13.0", false}, {"\U0001fa79", "adhesive bandage", []string{"adhesive_bandage"}, "12.0", false}, {"\U0001f9d1", "person", []string{"adult"}, "11.0", true}, {"\U0001f6a1", "aerial tramway", []string{"aerial_tramway"}, "6.0", false}, @@ -35,6 +36,7 @@ var GemojiData = Gemoji{ {"\U0001f691", "ambulance", []string{"ambulance"}, "6.0", false}, {"\U0001f1e6\U0001f1f8", "flag: American Samoa", []string{"american_samoa"}, "6.0", false}, {"\U0001f3fa", "amphora", []string{"amphora"}, "8.0", false}, + {"\U0001fac0", "anatomical heart", []string{"anatomical_heart"}, "13.0", false}, {"\u2693", "anchor", []string{"anchor"}, "4.1", false}, {"\U0001f1e6\U0001f1e9", "flag: Andorra", []string{"andorra"}, "6.0", false}, {"\U0001f47c", "baby angel", []string{"angel"}, "6.0", true}, @@ -127,17 +129,21 @@ var GemojiData = Gemoji{ {"\U0001f6c1", "bathtub", []string{"bathtub"}, "6.0", false}, {"\U0001f50b", "battery", []string{"battery"}, "6.0", false}, {"\U0001f3d6\ufe0f", "beach with umbrella", []string{"beach_umbrella"}, "7.0", false}, + {"\U0001fad8", "beans", []string{"beans"}, "14.0", false}, {"\U0001f43b", "bear", []string{"bear"}, "6.0", false}, {"\U0001f9d4", "person: beard", []string{"bearded_person"}, "11.0", true}, + {"\U0001f9ab", "beaver", []string{"beaver"}, "13.0", false}, {"\U0001f6cf\ufe0f", "bed", []string{"bed"}, "7.0", false}, {"\U0001f41d", "honeybee", []string{"bee", "honeybee"}, "6.0", false}, {"\U0001f37a", "beer mug", []string{"beer"}, "6.0", false}, {"\U0001f37b", "clinking beer mugs", []string{"beers"}, "6.0", false}, + {"\U0001fab2", "beetle", []string{"beetle"}, "13.0", false}, {"\U0001f530", "Japanese symbol for beginner", []string{"beginner"}, "6.0", false}, {"\U0001f1e7\U0001f1fe", "flag: Belarus", []string{"belarus"}, "6.0", false}, {"\U0001f1e7\U0001f1ea", "flag: Belgium", []string{"belgium"}, "6.0", false}, {"\U0001f1e7\U0001f1ff", "flag: Belize", []string{"belize"}, "6.0", false}, {"\U0001f514", "bell", []string{"bell"}, "6.0", false}, + {"\U0001fad1", "bell pepper", []string{"bell_pepper"}, "13.0", false}, {"\U0001f6ce\ufe0f", "bellhop bell", []string{"bellhop_bell"}, "7.0", false}, {"\U0001f1e7\U0001f1ef", "flag: Benin", []string{"benin"}, "6.0", false}, {"\U0001f371", "bento box", []string{"bento"}, "6.0", false}, @@ -153,6 +159,9 @@ var GemojiData = Gemoji{ {"\u2623\ufe0f", "biohazard", []string{"biohazard"}, "", false}, {"\U0001f426", "bird", []string{"bird"}, "6.0", false}, {"\U0001f382", "birthday cake", []string{"birthday"}, "6.0", false}, + {"\U0001f9ac", "bison", []string{"bison"}, "13.0", false}, + {"\U0001fae6", "biting lip", []string{"biting_lip"}, "14.0", false}, + {"\U0001f408\u200d\u2b1b", "black cat", []string{"black_cat"}, "13.0", false}, {"\u26ab", "black circle", []string{"black_circle"}, "4.1", false}, {"\U0001f3f4", "black flag", []string{"black_flag"}, "7.0", false}, {"\U0001f5a4", "black heart", []string{"black_heart"}, "9.0", false}, @@ -172,6 +181,7 @@ var GemojiData = Gemoji{ {"\U0001f699", "sport utility vehicle", []string{"blue_car"}, "6.0", false}, {"\U0001f499", "blue heart", []string{"blue_heart"}, "6.0", false}, {"\U0001f7e6", "blue square", []string{"blue_square"}, "12.0", false}, + {"\U0001fad0", "blueberries", []string{"blueberries"}, "13.0", false}, {"\U0001f60a", "smiling face with smiling eyes", []string{"blush"}, "6.0", false}, {"\U0001f417", "boar", []string{"boar"}, "6.0", false}, {"\u26f5", "sailboat", []string{"boat", "sailboat"}, "5.2", false}, @@ -183,6 +193,7 @@ var GemojiData = Gemoji{ {"\U0001f4d1", "bookmark tabs", []string{"bookmark_tabs"}, "6.0", false}, {"\U0001f4da", "books", []string{"books"}, "6.0", false}, {"\U0001f4a5", "collision", []string{"boom", "collision"}, "6.0", false}, + {"\U0001fa83", "boomerang", []string{"boomerang"}, "13.0", false}, {"\U0001f462", "woman’s boot", []string{"boot"}, "6.0", false}, {"\U0001f1e7\U0001f1e6", "flag: Bosnia & Herzegovina", []string{"bosnia_herzegovina"}, "6.0", false}, {"\U0001f1e7\U0001f1fc", "flag: Botswana", []string{"botswana"}, "6.0", false}, @@ -215,6 +226,9 @@ var GemojiData = Gemoji{ {"\U0001f90e", "brown heart", []string{"brown_heart"}, "12.0", false}, {"\U0001f7eb", "brown square", []string{"brown_square"}, "12.0", false}, {"\U0001f1e7\U0001f1f3", "flag: Brunei", []string{"brunei"}, "6.0", false}, + {"\U0001f9cb", "bubble tea", []string{"bubble_tea"}, "13.0", false}, + {"\U0001fae7", "bubbles", []string{"bubbles"}, "14.0", false}, + {"\U0001faa3", "bucket", []string{"bucket"}, "13.0", false}, {"\U0001f41b", "bug", []string{"bug"}, "6.0", false}, {"\U0001f3d7\ufe0f", "building construction", []string{"building_construction"}, "7.0", false}, {"\U0001f4a1", "light bulb", []string{"bulb"}, "6.0", false}, @@ -258,6 +272,7 @@ var GemojiData = Gemoji{ {"\U0001f5c2\ufe0f", "card index dividers", []string{"card_index_dividers"}, "7.0", false}, {"\U0001f1e7\U0001f1f6", "flag: Caribbean Netherlands", []string{"caribbean_netherlands"}, "6.0", false}, {"\U0001f3a0", "carousel horse", []string{"carousel_horse"}, "6.0", false}, + {"\U0001fa9a", "carpentry saw", []string{"carpentry_saw"}, "13.0", false}, {"\U0001f955", "carrot", []string{"carrot"}, "9.0", false}, {"\U0001f938", "person cartwheeling", []string{"cartwheeling"}, "11.0", true}, {"\U0001f431", "cat face", []string{"cat"}, "6.0", false}, @@ -341,11 +356,13 @@ var GemojiData = Gemoji{ {"\u2663\ufe0f", "club suit", []string{"clubs"}, "", false}, {"\U0001f1e8\U0001f1f3", "flag: China", []string{"cn"}, "6.0", false}, {"\U0001f9e5", "coat", []string{"coat"}, "11.0", false}, + {"\U0001fab3", "cockroach", []string{"cockroach"}, "13.0", false}, {"\U0001f378", "cocktail glass", []string{"cocktail"}, "6.0", false}, {"\U0001f965", "coconut", []string{"coconut"}, "11.0", false}, {"\U0001f1e8\U0001f1e8", "flag: Cocos (Keeling) Islands", []string{"cocos_islands"}, "6.0", false}, {"\u2615", "hot beverage", []string{"coffee"}, "4.0", false}, {"\u26b0\ufe0f", "coffin", []string{"coffin"}, "4.1", false}, + {"\U0001fa99", "coin", []string{"coin"}, "13.0", false}, {"\U0001f976", "cold face", []string{"cold_face"}, "11.0", false}, {"\U0001f630", "anxious face with sweat", []string{"cold_sweat"}, "6.0", false}, {"\U0001f1e8\U0001f1f4", "flag: Colombia", []string{"colombia"}, "6.0", false}, @@ -371,6 +388,7 @@ var GemojiData = Gemoji{ {"\U0001f36a", "cookie", []string{"cookie"}, "6.0", false}, {"\U0001f192", "COOL button", []string{"cool"}, "6.0", false}, {"\u00a9\ufe0f", "copyright", []string{"copyright"}, "", false}, + {"\U0001fab8", "coral", []string{"coral"}, "14.0", false}, {"\U0001f33d", "ear of corn", []string{"corn"}, "6.0", false}, {"\U0001f1e8\U0001f1f7", "flag: Costa Rica", []string{"costa_rica"}, "6.0", false}, {"\U0001f1e8\U0001f1ee", "flag: Côte d’Ivoire", []string{"cote_divoire"}, "6.0", false}, @@ -400,6 +418,7 @@ var GemojiData = Gemoji{ {"\U0001f38c", "crossed flags", []string{"crossed_flags"}, "6.0", false}, {"\u2694\ufe0f", "crossed swords", []string{"crossed_swords"}, "4.1", false}, {"\U0001f451", "crown", []string{"crown"}, "6.0", false}, + {"\U0001fa7c", "crutch", []string{"crutch"}, "14.0", false}, {"\U0001f622", "crying face", []string{"cry"}, "6.0", false}, {"\U0001f63f", "crying cat", []string{"crying_cat_face"}, "6.0", false}, {"\U0001f52e", "crystal ball", []string{"crystal_ball"}, "6.0", false}, @@ -449,13 +468,15 @@ var GemojiData = Gemoji{ {"\U0001f1e9\U0001f1ec", "flag: Diego Garcia", []string{"diego_garcia"}, "11.0", false}, {"\U0001f61e", "disappointed face", []string{"disappointed"}, "6.0", false}, {"\U0001f625", "sad but relieved face", []string{"disappointed_relieved"}, "6.0", false}, + {"\U0001f978", "disguised face", []string{"disguised_face"}, "13.0", false}, {"\U0001f93f", "diving mask", []string{"diving_mask"}, "12.0", false}, {"\U0001fa94", "diya lamp", []string{"diya_lamp"}, "12.0", false}, {"\U0001f4ab", "dizzy", []string{"dizzy"}, "6.0", false}, - {"\U0001f635", "knocked-out face", []string{"dizzy_face"}, "6.0", false}, + {"\U0001f635", "face with crossed-out eyes", []string{"dizzy_face"}, "6.0", false}, {"\U0001f1e9\U0001f1ef", "flag: Djibouti", []string{"djibouti"}, "6.0", false}, {"\U0001f9ec", "dna", []string{"dna"}, "11.0", false}, {"\U0001f6af", "no littering", []string{"do_not_litter"}, "6.0", false}, + {"\U0001f9a4", "dodo", []string{"dodo"}, "13.0", false}, {"\U0001f436", "dog face", []string{"dog"}, "6.0", false}, {"\U0001f415", "dog", []string{"dog2"}, "6.0", false}, {"\U0001f4b5", "dollar banknote", []string{"dollar"}, "6.0", false}, @@ -464,6 +485,7 @@ var GemojiData = Gemoji{ {"\U0001f1e9\U0001f1f2", "flag: Dominica", []string{"dominica"}, "6.0", false}, {"\U0001f1e9\U0001f1f4", "flag: Dominican Republic", []string{"dominican_republic"}, "6.0", false}, {"\U0001f6aa", "door", []string{"door"}, "6.0", false}, + {"\U0001fae5", "dotted line face", []string{"dotted_line_face"}, "14.0", false}, {"\U0001f369", "doughnut", []string{"doughnut"}, "6.0", false}, {"\U0001f54a\ufe0f", "dove", []string{"dove"}, "7.0", false}, {"\U0001f409", "dragon", []string{"dragon"}, "6.0", false}, @@ -495,10 +517,12 @@ var GemojiData = Gemoji{ {"\U0001f1f8\U0001f1fb", "flag: El Salvador", []string{"el_salvador"}, "6.0", false}, {"\U0001f50c", "electric plug", []string{"electric_plug"}, "6.0", false}, {"\U0001f418", "elephant", []string{"elephant"}, "6.0", false}, + {"\U0001f6d7", "elevator", []string{"elevator"}, "13.0", false}, {"\U0001f9dd", "elf", []string{"elf"}, "11.0", true}, {"\U0001f9dd\u200d\u2642\ufe0f", "man elf", []string{"elf_man"}, "11.0", true}, {"\U0001f9dd\u200d\u2640\ufe0f", "woman elf", []string{"elf_woman"}, "11.0", true}, {"\U0001f4e7", "e-mail", []string{"email", "e-mail"}, "6.0", false}, + {"\U0001fab9", "empty nest", []string{"empty_nest"}, "14.0", false}, {"\U0001f51a", "END arrow", []string{"end"}, "6.0", false}, {"\U0001f3f4\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f", "flag: England", []string{"england"}, "11.0", false}, {"\u2709\ufe0f", "envelope", []string{"envelope"}, "", false}, @@ -520,7 +544,14 @@ var GemojiData = Gemoji{ {"\U0001f441\ufe0f\u200d\U0001f5e8\ufe0f", "eye in speech bubble", []string{"eye_speech_bubble"}, "11.0", false}, {"\U0001f453", "glasses", []string{"eyeglasses"}, "6.0", false}, {"\U0001f440", "eyes", []string{"eyes"}, "6.0", false}, + {"\U0001f62e\u200d\U0001f4a8", "face exhaling", []string{"face_exhaling"}, "13.1", false}, + {"\U0001f979", "face holding back tears", []string{"face_holding_back_tears"}, "14.0", false}, + {"\U0001f636\u200d\U0001f32b\ufe0f", "face in clouds", []string{"face_in_clouds"}, "13.1", false}, + {"\U0001fae4", "face with diagonal mouth", []string{"face_with_diagonal_mouth"}, "14.0", false}, {"\U0001f915", "face with head-bandage", []string{"face_with_head_bandage"}, "8.0", false}, + {"\U0001fae2", "face with open eyes and hand over mouth", []string{"face_with_open_eyes_and_hand_over_mouth"}, "14.0", false}, + {"\U0001fae3", "face with peeking eye", []string{"face_with_peeking_eye"}, "14.0", false}, + {"\U0001f635\u200d\U0001f4ab", "face with spiral eyes", []string{"face_with_spiral_eyes"}, "13.1", false}, {"\U0001f912", "face with thermometer", []string{"face_with_thermometer"}, "8.0", false}, {"\U0001f926", "person facepalming", []string{"facepalm"}, "11.0", true}, {"\U0001f3ed", "factory", []string{"factory"}, "6.0", false}, @@ -562,6 +593,7 @@ var GemojiData = Gemoji{ {"\u23e9", "fast-forward button", []string{"fast_forward"}, "6.0", false}, {"\U0001f4e0", "fax machine", []string{"fax"}, "6.0", false}, {"\U0001f628", "fearful face", []string{"fearful"}, "6.0", false}, + {"\U0001fab6", "feather", []string{"feather"}, "13.0", false}, {"\U0001f43e", "paw prints", []string{"feet", "paw_prints"}, "6.0", false}, {"\U0001f575\ufe0f\u200d\u2640\ufe0f", "woman detective", []string{"female_detective"}, "6.0", true}, {"\u2640\ufe0f", "female sign", []string{"female_sign"}, "11.0", false}, @@ -594,16 +626,19 @@ var GemojiData = Gemoji{ {"\U0001f9a9", "flamingo", []string{"flamingo"}, "12.0", false}, {"\U0001f526", "flashlight", []string{"flashlight"}, "6.0", false}, {"\U0001f97f", "flat shoe", []string{"flat_shoe"}, "11.0", false}, + {"\U0001fad3", "flatbread", []string{"flatbread"}, "13.0", false}, {"\u269c\ufe0f", "fleur-de-lis", []string{"fleur_de_lis"}, "4.1", false}, {"\U0001f6ec", "airplane arrival", []string{"flight_arrival"}, "7.0", false}, {"\U0001f6eb", "airplane departure", []string{"flight_departure"}, "7.0", false}, {"\U0001f4be", "floppy disk", []string{"floppy_disk"}, "6.0", false}, {"\U0001f3b4", "flower playing cards", []string{"flower_playing_cards"}, "6.0", false}, {"\U0001f633", "flushed face", []string{"flushed"}, "6.0", false}, + {"\U0001fab0", "fly", []string{"fly"}, "13.0", false}, {"\U0001f94f", "flying disc", []string{"flying_disc"}, "11.0", false}, {"\U0001f6f8", "flying saucer", []string{"flying_saucer"}, "11.0", false}, {"\U0001f32b\ufe0f", "fog", []string{"fog"}, "7.0", false}, {"\U0001f301", "foggy", []string{"foggy"}, "6.0", false}, + {"\U0001fad5", "fondue", []string{"fondue"}, "13.0", false}, {"\U0001f9b6", "foot", []string{"foot"}, "11.0", true}, {"\U0001f3c8", "american football", []string{"football"}, "6.0", false}, {"\U0001f463", "footprints", []string{"footprints"}, "6.0", false}, @@ -698,17 +733,20 @@ var GemojiData = Gemoji{ {"\U0001f528", "hammer", []string{"hammer"}, "6.0", false}, {"\u2692\ufe0f", "hammer and pick", []string{"hammer_and_pick"}, "4.1", false}, {"\U0001f6e0\ufe0f", "hammer and wrench", []string{"hammer_and_wrench"}, "7.0", false}, + {"\U0001faac", "hamsa", []string{"hamsa"}, "14.0", false}, {"\U0001f439", "hamster", []string{"hamster"}, "6.0", false}, {"\u270b", "raised hand", []string{"hand", "raised_hand"}, "6.0", true}, {"\U0001f92d", "face with hand over mouth", []string{"hand_over_mouth"}, "11.0", false}, + {"\U0001faf0", "hand with index finger and thumb crossed", []string{"hand_with_index_finger_and_thumb_crossed"}, "14.0", true}, {"\U0001f45c", "handbag", []string{"handbag"}, "6.0", false}, {"\U0001f93e", "person playing handball", []string{"handball_person"}, "11.0", true}, - {"\U0001f91d", "handshake", []string{"handshake"}, "9.0", false}, + {"\U0001f91d", "handshake", []string{"handshake"}, "9.0", true}, {"\U0001f4a9", "pile of poo", []string{"hankey", "poop", "shit"}, "6.0", false}, {"#\ufe0f\u20e3", "keycap: #", []string{"hash"}, "", false}, {"\U0001f425", "front-facing baby chick", []string{"hatched_chick"}, "6.0", false}, {"\U0001f423", "hatching chick", []string{"hatching_chick"}, "6.0", false}, {"\U0001f3a7", "headphone", []string{"headphones"}, "6.0", false}, + {"\U0001faa6", "headstone", []string{"headstone"}, "13.0", false}, {"\U0001f9d1\u200d\u2695\ufe0f", "health worker", []string{"health_worker"}, "12.1", true}, {"\U0001f649", "hear-no-evil monkey", []string{"hear_no_evil"}, "6.0", false}, {"\U0001f1ed\U0001f1f2", "flag: Heard & McDonald Islands", []string{"heard_mcdonald_islands"}, "11.0", false}, @@ -716,12 +754,15 @@ var GemojiData = Gemoji{ {"\U0001f49f", "heart decoration", []string{"heart_decoration"}, "6.0", false}, {"\U0001f60d", "smiling face with heart-eyes", []string{"heart_eyes"}, "6.0", false}, {"\U0001f63b", "smiling cat with heart-eyes", []string{"heart_eyes_cat"}, "6.0", false}, + {"\U0001faf6", "heart hands", []string{"heart_hands"}, "14.0", true}, + {"\u2764\ufe0f\u200d\U0001f525", "heart on fire", []string{"heart_on_fire"}, "13.1", false}, {"\U0001f493", "beating heart", []string{"heartbeat"}, "6.0", false}, {"\U0001f497", "growing heart", []string{"heartpulse"}, "6.0", false}, {"\u2665\ufe0f", "heart suit", []string{"hearts"}, "", false}, {"\u2714\ufe0f", "check mark", []string{"heavy_check_mark"}, "", false}, {"\u2797", "divide", []string{"heavy_division_sign"}, "6.0", false}, {"\U0001f4b2", "heavy dollar sign", []string{"heavy_dollar_sign"}, "6.0", false}, + {"\U0001f7f0", "heavy equals sign", []string{"heavy_equals_sign"}, "14.0", false}, {"\u2763\ufe0f", "heart exclamation", []string{"heavy_heart_exclamation"}, "", false}, {"\u2796", "minus", []string{"heavy_minus_sign"}, "6.0", false}, {"\u2716\ufe0f", "multiply", []string{"heavy_multiplication_x"}, "", false}, @@ -740,6 +781,7 @@ var GemojiData = Gemoji{ {"\U0001f1ed\U0001f1f3", "flag: Honduras", []string{"honduras"}, "6.0", false}, {"\U0001f36f", "honey pot", []string{"honey_pot"}, "6.0", false}, {"\U0001f1ed\U0001f1f0", "flag: Hong Kong SAR China", []string{"hong_kong"}, "6.0", false}, + {"\U0001fa9d", "hook", []string{"hook"}, "13.0", false}, {"\U0001f434", "horse face", []string{"horse"}, "6.0", false}, {"\U0001f3c7", "horse racing", []string{"horse_racing"}, "6.0", true}, {"\U0001f3e5", "hospital", []string{"hospital"}, "6.0", false}, @@ -753,9 +795,10 @@ var GemojiData = Gemoji{ {"\U0001f3e0", "house", []string{"house"}, "6.0", false}, {"\U0001f3e1", "house with garden", []string{"house_with_garden"}, "6.0", false}, {"\U0001f3d8\ufe0f", "houses", []string{"houses"}, "7.0", false}, - {"\U0001f917", "hugging face", []string{"hugs"}, "8.0", false}, + {"\U0001f917", "smiling face with open hands", []string{"hugs"}, "8.0", false}, {"\U0001f1ed\U0001f1fa", "flag: Hungary", []string{"hungary"}, "6.0", false}, {"\U0001f62f", "hushed face", []string{"hushed"}, "6.1", false}, + {"\U0001f6d6", "hut", []string{"hut"}, "13.0", false}, {"\U0001f368", "ice cream", []string{"ice_cream"}, "6.0", false}, {"\U0001f9ca", "ice", []string{"ice_cube"}, "12.0", false}, {"\U0001f3d2", "ice hockey", []string{"ice_hockey"}, "8.0", false}, @@ -763,10 +806,12 @@ var GemojiData = Gemoji{ {"\U0001f366", "soft ice cream", []string{"icecream"}, "6.0", false}, {"\U0001f1ee\U0001f1f8", "flag: Iceland", []string{"iceland"}, "6.0", false}, {"\U0001f194", "ID button", []string{"id"}, "6.0", false}, + {"\U0001faaa", "identification card", []string{"identification_card"}, "14.0", false}, {"\U0001f250", "Japanese “bargain” button", []string{"ideograph_advantage"}, "6.0", false}, {"\U0001f47f", "angry face with horns", []string{"imp"}, "6.0", false}, {"\U0001f4e5", "inbox tray", []string{"inbox_tray"}, "6.0", false}, {"\U0001f4e8", "incoming envelope", []string{"incoming_envelope"}, "6.0", false}, + {"\U0001faf5", "index pointing at the viewer", []string{"index_pointing_at_the_viewer"}, "14.0", true}, {"\U0001f1ee\U0001f1f3", "flag: India", []string{"india"}, "6.0", false}, {"\U0001f1ee\U0001f1e9", "flag: Indonesia", []string{"indonesia"}, "6.0", false}, {"\u267e\ufe0f", "infinity", []string{"infinity"}, "11.0", false}, @@ -787,6 +832,7 @@ var GemojiData = Gemoji{ {"\U0001f3ef", "Japanese castle", []string{"japanese_castle"}, "6.0", false}, {"\U0001f47a", "goblin", []string{"japanese_goblin"}, "6.0", false}, {"\U0001f479", "ogre", []string{"japanese_ogre"}, "6.0", false}, + {"\U0001fad9", "jar", []string{"jar"}, "14.0", false}, {"\U0001f456", "jeans", []string{"jeans"}, "6.0", false}, {"\U0001f1ef\U0001f1ea", "flag: Jersey", []string{"jersey"}, "6.0", false}, {"\U0001f9e9", "puzzle piece", []string{"jigsaw"}, "11.0", false}, @@ -818,6 +864,7 @@ var GemojiData = Gemoji{ {"\U0001f9ce\u200d\u2642\ufe0f", "man kneeling", []string{"kneeling_man"}, "12.0", true}, {"\U0001f9ce", "person kneeling", []string{"kneeling_person"}, "12.0", true}, {"\U0001f9ce\u200d\u2640\ufe0f", "woman kneeling", []string{"kneeling_woman"}, "12.0", true}, + {"\U0001faa2", "knot", []string{"knot"}, "13.0", false}, {"\U0001f428", "koala", []string{"koala"}, "6.0", false}, {"\U0001f201", "Japanese “here” button", []string{"koko"}, "6.0", false}, {"\U0001f1fd\U0001f1f0", "flag: Kosovo", []string{"kosovo"}, "6.0", false}, @@ -827,6 +874,7 @@ var GemojiData = Gemoji{ {"\U0001f97c", "lab coat", []string{"lab_coat"}, "11.0", false}, {"\U0001f3f7\ufe0f", "label", []string{"label"}, "7.0", false}, {"\U0001f94d", "lacrosse", []string{"lacrosse"}, "11.0", false}, + {"\U0001fa9c", "ladder", []string{"ladder"}, "13.0", false}, {"\U0001f41e", "lady beetle", []string{"lady_beetle"}, "6.0", false}, {"\U0001f1f1\U0001f1e6", "flag: Laos", []string{"laos"}, "6.0", false}, {"\U0001f535", "blue circle", []string{"large_blue_circle"}, "6.0", false}, @@ -845,6 +893,7 @@ var GemojiData = Gemoji{ {"\u2194\ufe0f", "left-right arrow", []string{"left_right_arrow"}, "", false}, {"\U0001f5e8\ufe0f", "left speech bubble", []string{"left_speech_bubble"}, "11.0", false}, {"\u21a9\ufe0f", "right arrow curving left", []string{"leftwards_arrow_with_hook"}, "", false}, + {"\U0001faf2", "leftwards hand", []string{"leftwards_hand"}, "14.0", true}, {"\U0001f9b5", "leg", []string{"leg"}, "11.0", true}, {"\U0001f34b", "lemon", []string{"lemon"}, "6.0", false}, {"\u264c", "Leo", []string{"leo"}, "", false}, @@ -867,8 +916,10 @@ var GemojiData = Gemoji{ {"\U0001f512", "locked", []string{"lock"}, "6.0", false}, {"\U0001f50f", "locked with pen", []string{"lock_with_ink_pen"}, "6.0", false}, {"\U0001f36d", "lollipop", []string{"lollipop"}, "6.0", false}, + {"\U0001fa98", "long drum", []string{"long_drum"}, "13.0", false}, {"\u27bf", "double curly loop", []string{"loop"}, "6.0", false}, {"\U0001f9f4", "lotion bottle", []string{"lotion_bottle"}, "11.0", false}, + {"\U0001fab7", "lotus", []string{"lotus"}, "14.0", false}, {"\U0001f9d8", "person in lotus position", []string{"lotus_position"}, "11.0", true}, {"\U0001f9d8\u200d\u2642\ufe0f", "man in lotus position", []string{"lotus_position_man"}, "11.0", true}, {"\U0001f9d8\u200d\u2640\ufe0f", "woman in lotus position", []string{"lotus_position_woman"}, "11.0", true}, @@ -877,8 +928,10 @@ var GemojiData = Gemoji{ {"\U0001f3e9", "love hotel", []string{"love_hotel"}, "6.0", false}, {"\U0001f48c", "love letter", []string{"love_letter"}, "6.0", false}, {"\U0001f91f", "love-you gesture", []string{"love_you_gesture"}, "11.0", true}, + {"\U0001faab", "low battery", []string{"low_battery"}, "14.0", false}, {"\U0001f505", "dim button", []string{"low_brightness"}, "6.0", false}, {"\U0001f9f3", "luggage", []string{"luggage"}, "11.0", false}, + {"\U0001fac1", "lungs", []string{"lungs"}, "13.0", false}, {"\U0001f1f1\U0001f1fa", "flag: Luxembourg", []string{"luxembourg"}, "6.0", false}, {"\U0001f925", "lying face", []string{"lying_face"}, "9.0", false}, {"\u24c2\ufe0f", "circled M", []string{"m"}, "", false}, @@ -890,6 +943,7 @@ var GemojiData = Gemoji{ {"\U0001f9d9", "mage", []string{"mage"}, "11.0", true}, {"\U0001f9d9\u200d\u2642\ufe0f", "man mage", []string{"mage_man"}, "11.0", true}, {"\U0001f9d9\u200d\u2640\ufe0f", "woman mage", []string{"mage_woman"}, "11.0", true}, + {"\U0001fa84", "magic wand", []string{"magic_wand"}, "13.0", false}, {"\U0001f9f2", "magnet", []string{"magnet"}, "11.0", false}, {"\U0001f004", "mahjong red dragon", []string{"mahjong"}, "", false}, {"\U0001f4eb", "closed mailbox with raised flag", []string{"mailbox"}, "6.0", false}, @@ -903,19 +957,23 @@ var GemojiData = Gemoji{ {"\u2642\ufe0f", "male sign", []string{"male_sign"}, "11.0", false}, {"\U0001f1f2\U0001f1f1", "flag: Mali", []string{"mali"}, "6.0", false}, {"\U0001f1f2\U0001f1f9", "flag: Malta", []string{"malta"}, "6.0", false}, + {"\U0001f9a3", "mammoth", []string{"mammoth"}, "13.0", false}, {"\U0001f468", "man", []string{"man"}, "6.0", true}, {"\U0001f468\u200d\U0001f3a8", "man artist", []string{"man_artist"}, "", true}, {"\U0001f468\u200d\U0001f680", "man astronaut", []string{"man_astronaut"}, "", true}, + {"\U0001f9d4\u200d\u2642\ufe0f", "man: beard", []string{"man_beard"}, "13.1", true}, {"\U0001f938\u200d\u2642\ufe0f", "man cartwheeling", []string{"man_cartwheeling"}, "", true}, {"\U0001f468\u200d\U0001f373", "man cook", []string{"man_cook"}, "", true}, {"\U0001f57a", "man dancing", []string{"man_dancing"}, "9.0", true}, {"\U0001f926\u200d\u2642\ufe0f", "man facepalming", []string{"man_facepalming"}, "9.0", true}, {"\U0001f468\u200d\U0001f3ed", "man factory worker", []string{"man_factory_worker"}, "", true}, {"\U0001f468\u200d\U0001f33e", "man farmer", []string{"man_farmer"}, "", true}, + {"\U0001f468\u200d\U0001f37c", "man feeding baby", []string{"man_feeding_baby"}, "13.0", true}, {"\U0001f468\u200d\U0001f692", "man firefighter", []string{"man_firefighter"}, "", true}, {"\U0001f468\u200d\u2695\ufe0f", "man health worker", []string{"man_health_worker"}, "", true}, {"\U0001f468\u200d\U0001f9bd", "man in manual wheelchair", []string{"man_in_manual_wheelchair"}, "12.0", true}, {"\U0001f468\u200d\U0001f9bc", "man in motorized wheelchair", []string{"man_in_motorized_wheelchair"}, "12.0", true}, + {"\U0001f935\u200d\u2642\ufe0f", "man in tuxedo", []string{"man_in_tuxedo"}, "13.0", true}, {"\U0001f468\u200d\u2696\ufe0f", "man judge", []string{"man_judge"}, "", true}, {"\U0001f939\u200d\u2642\ufe0f", "man juggling", []string{"man_juggling"}, "9.0", true}, {"\U0001f468\u200d\U0001f527", "man mechanic", []string{"man_mechanic"}, "", true}, @@ -932,6 +990,7 @@ var GemojiData = Gemoji{ {"\U0001f472", "person with skullcap", []string{"man_with_gua_pi_mao"}, "6.0", true}, {"\U0001f468\u200d\U0001f9af", "man with white cane", []string{"man_with_probing_cane"}, "12.0", true}, {"\U0001f473\u200d\u2642\ufe0f", "man wearing turban", []string{"man_with_turban"}, "11.0", true}, + {"\U0001f470\u200d\u2642\ufe0f", "man with veil", []string{"man_with_veil"}, "13.0", true}, {"\U0001f96d", "mango", []string{"mango"}, "11.0", false}, {"\U0001f45e", "man’s shoe", []string{"mans_shoe", "shoe"}, "6.0", false}, {"\U0001f570\ufe0f", "mantelpiece clock", []string{"mantelpiece_clock"}, "7.0", false}, @@ -957,8 +1016,10 @@ var GemojiData = Gemoji{ {"\u2695\ufe0f", "medical symbol", []string{"medical_symbol"}, "11.0", false}, {"\U0001f4e3", "megaphone", []string{"mega"}, "6.0", false}, {"\U0001f348", "melon", []string{"melon"}, "6.0", false}, + {"\U0001fae0", "melting face", []string{"melting_face"}, "14.0", false}, {"\U0001f4dd", "memo", []string{"memo", "pencil"}, "6.0", false}, {"\U0001f93c\u200d\u2642\ufe0f", "men wrestling", []string{"men_wrestling"}, "9.0", false}, + {"\u2764\ufe0f\u200d\U0001fa79", "mending heart", []string{"mending_heart"}, "13.1", false}, {"\U0001f54e", "menorah", []string{"menorah"}, "8.0", false}, {"\U0001f6b9", "men’s room", []string{"mens"}, "6.0", false}, {"\U0001f9dc\u200d\u2640\ufe0f", "mermaid", []string{"mermaid"}, "11.0", true}, @@ -972,10 +1033,13 @@ var GemojiData = Gemoji{ {"\U0001f3a4", "microphone", []string{"microphone"}, "6.0", false}, {"\U0001f52c", "microscope", []string{"microscope"}, "6.0", false}, {"\U0001f595", "middle finger", []string{"middle_finger", "fu"}, "7.0", true}, + {"\U0001fa96", "military helmet", []string{"military_helmet"}, "13.0", false}, {"\U0001f95b", "glass of milk", []string{"milk_glass"}, "9.0", false}, {"\U0001f30c", "milky way", []string{"milky_way"}, "6.0", false}, {"\U0001f690", "minibus", []string{"minibus"}, "6.0", false}, {"\U0001f4bd", "computer disk", []string{"minidisc"}, "6.0", false}, + {"\U0001fa9e", "mirror", []string{"mirror"}, "13.0", false}, + {"\U0001faa9", "mirror ball", []string{"mirror_ball"}, "14.0", false}, {"\U0001f4f4", "mobile phone off", []string{"mobile_phone_off"}, "6.0", false}, {"\U0001f1f2\U0001f1e9", "flag: Moldova", []string{"moldova"}, "6.0", false}, {"\U0001f1f2\U0001f1e8", "flag: Monaco", []string{"monaco"}, "6.0", false}, @@ -1010,6 +1074,7 @@ var GemojiData = Gemoji{ {"\U0001f3d4\ufe0f", "snow-capped mountain", []string{"mountain_snow"}, "7.0", false}, {"\U0001f42d", "mouse face", []string{"mouse"}, "6.0", false}, {"\U0001f401", "mouse", []string{"mouse2"}, "6.0", false}, + {"\U0001faa4", "mouse trap", []string{"mouse_trap"}, "13.0", false}, {"\U0001f3a5", "movie camera", []string{"movie_camera"}, "6.0", false}, {"\U0001f5ff", "moai", []string{"moyai"}, "6.0", false}, {"\U0001f1f2\U0001f1ff", "flag: Mozambique", []string{"mozambique"}, "6.0", false}, @@ -1020,6 +1085,7 @@ var GemojiData = Gemoji{ {"\U0001f3b5", "musical note", []string{"musical_note"}, "6.0", false}, {"\U0001f3bc", "musical score", []string{"musical_score"}, "6.0", false}, {"\U0001f507", "muted speaker", []string{"mute"}, "6.0", false}, + {"\U0001f9d1\u200d\U0001f384", "mx claus", []string{"mx_claus"}, "13.0", true}, {"\U0001f1f2\U0001f1f2", "flag: Myanmar (Burma)", []string{"myanmar"}, "6.0", false}, {"\U0001f485", "nail polish", []string{"nail_care"}, "6.0", true}, {"\U0001f4db", "name badge", []string{"name_badge"}, "6.0", false}, @@ -1032,6 +1098,8 @@ var GemojiData = Gemoji{ {"\u274e", "cross mark button", []string{"negative_squared_cross_mark"}, "6.0", false}, {"\U0001f1f3\U0001f1f5", "flag: Nepal", []string{"nepal"}, "6.0", false}, {"\U0001f913", "nerd face", []string{"nerd_face"}, "8.0", false}, + {"\U0001faba", "nest with eggs", []string{"nest_with_eggs"}, "14.0", false}, + {"\U0001fa86", "nesting dolls", []string{"nesting_dolls"}, "13.0", false}, {"\U0001f1f3\U0001f1f1", "flag: Netherlands", []string{"netherlands"}, "6.0", false}, {"\U0001f610", "neutral face", []string{"neutral_face"}, "6.0", false}, {"\U0001f195", "NEW button", []string{"new"}, "6.0", false}, @@ -1048,6 +1116,7 @@ var GemojiData = Gemoji{ {"\U0001f1f3\U0001f1ec", "flag: Nigeria", []string{"nigeria"}, "6.0", false}, {"\U0001f303", "night with stars", []string{"night_with_stars"}, "6.0", false}, {"9\ufe0f\u20e3", "keycap: 9", []string{"nine"}, "", false}, + {"\U0001f977", "ninja", []string{"ninja"}, "13.0", true}, {"\U0001f1f3\U0001f1fa", "flag: Niue", []string{"niue"}, "6.0", false}, {"\U0001f515", "bell with slash", []string{"no_bell"}, "6.0", false}, {"\U0001f6b3", "no bicycles", []string{"no_bicycles"}, "6.0", false}, @@ -1087,6 +1156,7 @@ var GemojiData = Gemoji{ {"\U0001f9d3", "older person", []string{"older_adult"}, "11.0", true}, {"\U0001f474", "old man", []string{"older_man"}, "6.0", true}, {"\U0001f475", "old woman", []string{"older_woman"}, "6.0", true}, + {"\U0001fad2", "olive", []string{"olive"}, "13.0", false}, {"\U0001f549\ufe0f", "om", []string{"om"}, "7.0", false}, {"\U0001f1f4\U0001f1f2", "flag: Oman", []string{"oman"}, "6.0", false}, {"\U0001f51b", "ON! arrow", []string{"on"}, "6.0", false}, @@ -1121,7 +1191,9 @@ var GemojiData = Gemoji{ {"\U0001f1f5\U0001f1f0", "flag: Pakistan", []string{"pakistan"}, "6.0", false}, {"\U0001f1f5\U0001f1fc", "flag: Palau", []string{"palau"}, "6.0", false}, {"\U0001f1f5\U0001f1f8", "flag: Palestinian Territories", []string{"palestinian_territories"}, "6.0", false}, + {"\U0001faf3", "palm down hand", []string{"palm_down_hand"}, "14.0", true}, {"\U0001f334", "palm tree", []string{"palm_tree"}, "6.0", false}, + {"\U0001faf4", "palm up hand", []string{"palm_up_hand"}, "14.0", true}, {"\U0001f932", "palms up together", []string{"palms_up_together"}, "11.0", true}, {"\U0001f1f5\U0001f1e6", "flag: Panama", []string{"panama"}, "6.0", false}, {"\U0001f95e", "pancakes", []string{"pancakes"}, "9.0", false}, @@ -1149,17 +1221,20 @@ var GemojiData = Gemoji{ {"\u270f\ufe0f", "pencil", []string{"pencil2"}, "", false}, {"\U0001f427", "penguin", []string{"penguin"}, "6.0", false}, {"\U0001f614", "pensive face", []string{"pensive"}, "6.0", false}, - {"\U0001f9d1\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands", []string{"people_holding_hands"}, "12.0", true}, + {"\U0001f9d1\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands", []string{"people_holding_hands"}, "12.0", false}, + {"\U0001fac2", "people hugging", []string{"people_hugging"}, "13.0", false}, {"\U0001f3ad", "performing arts", []string{"performing_arts"}, "6.0", false}, {"\U0001f623", "persevering face", []string{"persevere"}, "6.0", false}, {"\U0001f9d1\u200d\U0001f9b2", "person: bald", []string{"person_bald"}, "12.1", true}, {"\U0001f9d1\u200d\U0001f9b1", "person: curly hair", []string{"person_curly_hair"}, "12.1", true}, + {"\U0001f9d1\u200d\U0001f37c", "person feeding baby", []string{"person_feeding_baby"}, "13.0", true}, {"\U0001f93a", "person fencing", []string{"person_fencing"}, "9.0", false}, {"\U0001f9d1\u200d\U0001f9bd", "person in manual wheelchair", []string{"person_in_manual_wheelchair"}, "12.1", true}, {"\U0001f9d1\u200d\U0001f9bc", "person in motorized wheelchair", []string{"person_in_motorized_wheelchair"}, "12.1", true}, {"\U0001f935", "person in tuxedo", []string{"person_in_tuxedo"}, "9.0", true}, {"\U0001f9d1\u200d\U0001f9b0", "person: red hair", []string{"person_red_hair"}, "12.1", true}, {"\U0001f9d1\u200d\U0001f9b3", "person: white hair", []string{"person_white_hair"}, "12.1", true}, + {"\U0001fac5", "person with crown", []string{"person_with_crown"}, "14.0", true}, {"\U0001f9d1\u200d\U0001f9af", "person with white cane", []string{"person_with_probing_cane"}, "12.1", true}, {"\U0001f473", "person wearing turban", []string{"person_with_turban"}, "6.0", true}, {"\U0001f470", "person with veil", []string{"person_with_veil"}, "6.0", true}, @@ -1168,12 +1243,15 @@ var GemojiData = Gemoji{ {"\U0001f1f5\U0001f1ed", "flag: Philippines", []string{"philippines"}, "6.0", false}, {"\u260e\ufe0f", "telephone", []string{"phone", "telephone"}, "", false}, {"\u26cf\ufe0f", "pick", []string{"pick"}, "5.2", false}, + {"\U0001f6fb", "pickup truck", []string{"pickup_truck"}, "13.0", false}, {"\U0001f967", "pie", []string{"pie"}, "11.0", false}, {"\U0001f437", "pig face", []string{"pig"}, "6.0", false}, {"\U0001f416", "pig", []string{"pig2"}, "6.0", false}, {"\U0001f43d", "pig nose", []string{"pig_nose"}, "6.0", false}, {"\U0001f48a", "pill", []string{"pill"}, "6.0", false}, {"\U0001f9d1\u200d\u2708\ufe0f", "pilot", []string{"pilot"}, "12.1", true}, + {"\U0001fa85", "piñata", []string{"pinata"}, "13.0", false}, + {"\U0001f90c", "pinched fingers", []string{"pinched_fingers"}, "13.0", true}, {"\U0001f90f", "pinching hand", []string{"pinching_hand"}, "12.0", true}, {"\U0001f34d", "pineapple", []string{"pineapple"}, "6.0", false}, {"\U0001f3d3", "ping pong", []string{"ping_pong"}, "8.0", false}, @@ -1181,16 +1259,20 @@ var GemojiData = Gemoji{ {"\u2653", "Pisces", []string{"pisces"}, "", false}, {"\U0001f1f5\U0001f1f3", "flag: Pitcairn Islands", []string{"pitcairn_islands"}, "6.0", false}, {"\U0001f355", "pizza", []string{"pizza"}, "6.0", false}, + {"\U0001faa7", "placard", []string{"placard"}, "13.0", false}, {"\U0001f6d0", "place of worship", []string{"place_of_worship"}, "8.0", false}, {"\U0001f37d\ufe0f", "fork and knife with plate", []string{"plate_with_cutlery"}, "7.0", false}, {"\u23ef\ufe0f", "play or pause button", []string{"play_or_pause_button"}, "6.0", false}, + {"\U0001f6dd", "playground slide", []string{"playground_slide"}, "14.0", false}, {"\U0001f97a", "pleading face", []string{"pleading_face"}, "11.0", false}, + {"\U0001faa0", "plunger", []string{"plunger"}, "13.0", false}, {"\U0001f447", "backhand index pointing down", []string{"point_down"}, "6.0", true}, {"\U0001f448", "backhand index pointing left", []string{"point_left"}, "6.0", true}, {"\U0001f449", "backhand index pointing right", []string{"point_right"}, "6.0", true}, {"\u261d\ufe0f", "index pointing up", []string{"point_up"}, "", true}, {"\U0001f446", "backhand index pointing up", []string{"point_up_2"}, "6.0", true}, {"\U0001f1f5\U0001f1f1", "flag: Poland", []string{"poland"}, "6.0", false}, + {"\U0001f43b\u200d\u2744\ufe0f", "polar bear", []string{"polar_bear"}, "13.0", false}, {"\U0001f693", "police car", []string{"police_car"}, "6.0", false}, {"\U0001f46e", "police officer", []string{"police_officer", "cop"}, "6.0", true}, {"\U0001f46e\u200d\u2642\ufe0f", "man police officer", []string{"policeman"}, "11.0", true}, @@ -1203,15 +1285,19 @@ var GemojiData = Gemoji{ {"\U0001f4ee", "postbox", []string{"postbox"}, "6.0", false}, {"\U0001f6b0", "potable water", []string{"potable_water"}, "6.0", false}, {"\U0001f954", "potato", []string{"potato"}, "9.0", false}, + {"\U0001fab4", "potted plant", []string{"potted_plant"}, "13.0", false}, {"\U0001f45d", "clutch bag", []string{"pouch"}, "6.0", false}, {"\U0001f357", "poultry leg", []string{"poultry_leg"}, "6.0", false}, {"\U0001f4b7", "pound banknote", []string{"pound"}, "6.0", false}, + {"\U0001fad7", "pouring liquid", []string{"pouring_liquid"}, "14.0", false}, {"\U0001f63e", "pouting cat", []string{"pouting_cat"}, "6.0", false}, {"\U0001f64e", "person pouting", []string{"pouting_face"}, "6.0", true}, {"\U0001f64e\u200d\u2642\ufe0f", "man pouting", []string{"pouting_man"}, "6.0", true}, {"\U0001f64e\u200d\u2640\ufe0f", "woman pouting", []string{"pouting_woman"}, "11.0", true}, {"\U0001f64f", "folded hands", []string{"pray"}, "6.0", true}, {"\U0001f4ff", "prayer beads", []string{"prayer_beads"}, "8.0", false}, + {"\U0001fac3", "pregnant man", []string{"pregnant_man"}, "14.0", true}, + {"\U0001fac4", "pregnant person", []string{"pregnant_person"}, "14.0", true}, {"\U0001f930", "pregnant woman", []string{"pregnant_woman"}, "9.0", true}, {"\U0001f968", "pretzel", []string{"pretzel"}, "11.0", false}, {"\u23ee\ufe0f", "last track button", []string{"previous_track_button"}, "6.0", false}, @@ -1278,14 +1364,18 @@ var GemojiData = Gemoji{ {"\U0001f358", "rice cracker", []string{"rice_cracker"}, "6.0", false}, {"\U0001f391", "moon viewing ceremony", []string{"rice_scene"}, "6.0", false}, {"\U0001f5ef\ufe0f", "right anger bubble", []string{"right_anger_bubble"}, "7.0", false}, + {"\U0001faf1", "rightwards hand", []string{"rightwards_hand"}, "14.0", true}, {"\U0001f48d", "ring", []string{"ring"}, "6.0", false}, + {"\U0001f6df", "ring buoy", []string{"ring_buoy"}, "14.0", false}, {"\U0001fa90", "ringed planet", []string{"ringed_planet"}, "12.0", false}, {"\U0001f916", "robot", []string{"robot"}, "8.0", false}, + {"\U0001faa8", "rock", []string{"rock"}, "13.0", false}, {"\U0001f680", "rocket", []string{"rocket"}, "6.0", false}, {"\U0001f923", "rolling on the floor laughing", []string{"rofl"}, "9.0", false}, {"\U0001f644", "face with rolling eyes", []string{"roll_eyes"}, "8.0", false}, {"\U0001f9fb", "roll of paper", []string{"roll_of_paper"}, "11.0", false}, {"\U0001f3a2", "roller coaster", []string{"roller_coaster"}, "6.0", false}, + {"\U0001f6fc", "roller skate", []string{"roller_skate"}, "13.0", false}, {"\U0001f1f7\U0001f1f4", "flag: Romania", []string{"romania"}, "6.0", false}, {"\U0001f413", "rooster", []string{"rooster"}, "6.0", false}, {"\U0001f339", "rose", []string{"rose"}, "6.0", false}, @@ -1308,6 +1398,7 @@ var GemojiData = Gemoji{ {"\u2650", "Sagittarius", []string{"sagittarius"}, "", false}, {"\U0001f376", "sake", []string{"sake"}, "6.0", false}, {"\U0001f9c2", "salt", []string{"salt"}, "11.0", false}, + {"\U0001fae1", "saluting face", []string{"saluting_face"}, "14.0", false}, {"\U0001f1fc\U0001f1f8", "flag: Samoa", []string{"samoa"}, "6.0", false}, {"\U0001f1f8\U0001f1f2", "flag: San Marino", []string{"san_marino"}, "6.0", false}, {"\U0001f461", "woman’s sandal", []string{"sandal"}, "6.0", false}, @@ -1332,7 +1423,9 @@ var GemojiData = Gemoji{ {"\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f", "flag: Scotland", []string{"scotland"}, "11.0", false}, {"\U0001f631", "face screaming in fear", []string{"scream"}, "6.0", false}, {"\U0001f640", "weary cat", []string{"scream_cat"}, "6.0", false}, + {"\U0001fa9b", "screwdriver", []string{"screwdriver"}, "13.0", false}, {"\U0001f4dc", "scroll", []string{"scroll"}, "6.0", false}, + {"\U0001f9ad", "seal", []string{"seal"}, "13.0", false}, {"\U0001f4ba", "seat", []string{"seat"}, "6.0", false}, {"\u3299\ufe0f", "Japanese “secret” button", []string{"secret"}, "", false}, {"\U0001f648", "see-no-evil monkey", []string{"see_no_evil"}, "6.0", false}, @@ -1342,6 +1435,7 @@ var GemojiData = Gemoji{ {"\U0001f1f7\U0001f1f8", "flag: Serbia", []string{"serbia"}, "6.0", false}, {"\U0001f415\u200d\U0001f9ba", "service dog", []string{"service_dog"}, "12.0", false}, {"7\ufe0f\u20e3", "keycap: 7", []string{"seven"}, "", false}, + {"\U0001faa1", "sewing needle", []string{"sewing_needle"}, "13.0", false}, {"\U0001f1f8\U0001f1e8", "flag: Seychelles", []string{"seychelles"}, "6.0", false}, {"\U0001f958", "shallow pan of food", []string{"shallow_pan_of_food"}, "", false}, {"\u2618\ufe0f", "shamrock", []string{"shamrock"}, "4.1", false}, @@ -1392,6 +1486,7 @@ var GemojiData = Gemoji{ {"\U0001f638", "grinning cat with smiling eyes", []string{"smile_cat"}, "6.0", false}, {"\U0001f603", "grinning face with big eyes", []string{"smiley"}, "6.0", false}, {"\U0001f63a", "grinning cat", []string{"smiley_cat"}, "6.0", false}, + {"\U0001f972", "smiling face with tear", []string{"smiling_face_with_tear"}, "13.0", false}, {"\U0001f970", "smiling face with hearts", []string{"smiling_face_with_three_hearts"}, "11.0", false}, {"\U0001f608", "smiling face with horns", []string{"smiling_imp"}, "6.0", false}, {"\U0001f60f", "smirking face", []string{"smirk"}, "6.0", false}, @@ -1515,6 +1610,7 @@ var GemojiData = Gemoji{ {"\U0001f1f9\U0001f1fc", "flag: Taiwan", []string{"taiwan"}, "6.0", false}, {"\U0001f1f9\U0001f1ef", "flag: Tajikistan", []string{"tajikistan"}, "6.0", false}, {"\U0001f961", "takeout box", []string{"takeout_box"}, "11.0", false}, + {"\U0001fad4", "tamale", []string{"tamale"}, "13.0", false}, {"\U0001f38b", "tanabata tree", []string{"tanabata_tree"}, "6.0", false}, {"\U0001f34a", "tangerine", []string{"tangerine", "orange", "mandarin"}, "6.0", false}, {"\U0001f1f9\U0001f1ff", "flag: Tanzania", []string{"tanzania"}, "6.0", false}, @@ -1522,6 +1618,7 @@ var GemojiData = Gemoji{ {"\U0001f695", "taxi", []string{"taxi"}, "6.0", false}, {"\U0001f375", "teacup without handle", []string{"tea"}, "6.0", false}, {"\U0001f9d1\u200d\U0001f3eb", "teacher", []string{"teacher"}, "12.1", true}, + {"\U0001fad6", "teapot", []string{"teapot"}, "13.0", false}, {"\U0001f9d1\u200d\U0001f4bb", "technologist", []string{"technologist"}, "12.1", true}, {"\U0001f9f8", "teddy bear", []string{"teddy_bear"}, "11.0", false}, {"\U0001f4de", "telephone receiver", []string{"telephone_receiver"}, "6.0", false}, @@ -1532,6 +1629,7 @@ var GemojiData = Gemoji{ {"\U0001f1f9\U0001f1ed", "flag: Thailand", []string{"thailand"}, "6.0", false}, {"\U0001f321\ufe0f", "thermometer", []string{"thermometer"}, "7.0", false}, {"\U0001f914", "thinking face", []string{"thinking"}, "8.0", false}, + {"\U0001fa74", "thong sandal", []string{"thong_sandal"}, "13.0", false}, {"\U0001f4ad", "thought balloon", []string{"thought_balloon"}, "6.0", false}, {"\U0001f9f5", "thread", []string{"thread"}, "11.0", false}, {"3\ufe0f\u20e3", "keycap: 3", []string{"three"}, "", false}, @@ -1555,6 +1653,7 @@ var GemojiData = Gemoji{ {"\U0001f445", "tongue", []string{"tongue"}, "6.0", false}, {"\U0001f9f0", "toolbox", []string{"toolbox"}, "11.0", false}, {"\U0001f9b7", "tooth", []string{"tooth"}, "11.0", false}, + {"\U0001faa5", "toothbrush", []string{"toothbrush"}, "13.0", false}, {"\U0001f51d", "TOP arrow", []string{"top"}, "6.0", false}, {"\U0001f3a9", "top hat", []string{"tophat"}, "6.0", false}, {"\U0001f32a\ufe0f", "tornado", []string{"tornado"}, "7.0", false}, @@ -1565,12 +1664,15 @@ var GemojiData = Gemoji{ {"\U0001f68b", "tram car", []string{"train"}, "6.0", false}, {"\U0001f686", "train", []string{"train2"}, "6.0", false}, {"\U0001f68a", "tram", []string{"tram"}, "6.0", false}, + {"\U0001f3f3\ufe0f\u200d\u26a7\ufe0f", "transgender flag", []string{"transgender_flag"}, "13.0", false}, + {"\u26a7\ufe0f", "transgender symbol", []string{"transgender_symbol"}, "13.0", false}, {"\U0001f6a9", "triangular flag", []string{"triangular_flag_on_post"}, "6.0", false}, {"\U0001f4d0", "triangular ruler", []string{"triangular_ruler"}, "6.0", false}, {"\U0001f531", "trident emblem", []string{"trident"}, "6.0", false}, {"\U0001f1f9\U0001f1f9", "flag: Trinidad & Tobago", []string{"trinidad_tobago"}, "6.0", false}, {"\U0001f1f9\U0001f1e6", "flag: Tristan da Cunha", []string{"tristan_da_cunha"}, "11.0", false}, {"\U0001f624", "face with steam from nose", []string{"triumph"}, "6.0", false}, + {"\U0001f9cc", "troll", []string{"troll"}, "14.0", false}, {"\U0001f68e", "trolleybus", []string{"trolleybus"}, "6.0", false}, {"\U0001f3c6", "trophy", []string{"trophy"}, "6.0", false}, {"\U0001f379", "tropical drink", []string{"tropical_drink"}, "6.0", false}, @@ -1664,6 +1766,7 @@ var GemojiData = Gemoji{ {"\U0001f1ea\U0001f1ed", "flag: Western Sahara", []string{"western_sahara"}, "6.0", false}, {"\U0001f433", "spouting whale", []string{"whale"}, "6.0", false}, {"\U0001f40b", "whale", []string{"whale2"}, "6.0", false}, + {"\U0001f6de", "wheel", []string{"wheel"}, "14.0", false}, {"\u2638\ufe0f", "wheel of dharma", []string{"wheel_of_dharma"}, "", false}, {"\u267f", "wheelchair symbol", []string{"wheelchair"}, "4.1", false}, {"\u2705", "check mark button", []string{"white_check_mark"}, "6.0", false}, @@ -1681,22 +1784,26 @@ var GemojiData = Gemoji{ {"\U0001f940", "wilted flower", []string{"wilted_flower"}, "9.0", false}, {"\U0001f390", "wind chime", []string{"wind_chime"}, "6.0", false}, {"\U0001f32c\ufe0f", "wind face", []string{"wind_face"}, "7.0", false}, + {"\U0001fa9f", "window", []string{"window"}, "13.0", false}, {"\U0001f377", "wine glass", []string{"wine_glass"}, "6.0", false}, {"\U0001f609", "winking face", []string{"wink"}, "6.0", false}, {"\U0001f43a", "wolf", []string{"wolf"}, "6.0", false}, {"\U0001f469", "woman", []string{"woman"}, "6.0", true}, {"\U0001f469\u200d\U0001f3a8", "woman artist", []string{"woman_artist"}, "", true}, {"\U0001f469\u200d\U0001f680", "woman astronaut", []string{"woman_astronaut"}, "", true}, + {"\U0001f9d4\u200d\u2640\ufe0f", "woman: beard", []string{"woman_beard"}, "13.1", true}, {"\U0001f938\u200d\u2640\ufe0f", "woman cartwheeling", []string{"woman_cartwheeling"}, "", true}, {"\U0001f469\u200d\U0001f373", "woman cook", []string{"woman_cook"}, "", true}, {"\U0001f483", "woman dancing", []string{"woman_dancing", "dancer"}, "6.0", true}, {"\U0001f926\u200d\u2640\ufe0f", "woman facepalming", []string{"woman_facepalming"}, "9.0", true}, {"\U0001f469\u200d\U0001f3ed", "woman factory worker", []string{"woman_factory_worker"}, "", true}, {"\U0001f469\u200d\U0001f33e", "woman farmer", []string{"woman_farmer"}, "", true}, + {"\U0001f469\u200d\U0001f37c", "woman feeding baby", []string{"woman_feeding_baby"}, "13.0", true}, {"\U0001f469\u200d\U0001f692", "woman firefighter", []string{"woman_firefighter"}, "", true}, {"\U0001f469\u200d\u2695\ufe0f", "woman health worker", []string{"woman_health_worker"}, "", true}, {"\U0001f469\u200d\U0001f9bd", "woman in manual wheelchair", []string{"woman_in_manual_wheelchair"}, "12.0", true}, {"\U0001f469\u200d\U0001f9bc", "woman in motorized wheelchair", []string{"woman_in_motorized_wheelchair"}, "12.0", true}, + {"\U0001f935\u200d\u2640\ufe0f", "woman in tuxedo", []string{"woman_in_tuxedo"}, "13.0", true}, {"\U0001f469\u200d\u2696\ufe0f", "woman judge", []string{"woman_judge"}, "", true}, {"\U0001f939\u200d\u2640\ufe0f", "woman juggling", []string{"woman_juggling"}, "9.0", true}, {"\U0001f469\u200d\U0001f527", "woman mechanic", []string{"woman_mechanic"}, "", true}, @@ -1713,17 +1820,21 @@ var GemojiData = Gemoji{ {"\U0001f9d5", "woman with headscarf", []string{"woman_with_headscarf"}, "11.0", true}, {"\U0001f469\u200d\U0001f9af", "woman with white cane", []string{"woman_with_probing_cane"}, "12.0", true}, {"\U0001f473\u200d\u2640\ufe0f", "woman wearing turban", []string{"woman_with_turban"}, "6.0", true}, + {"\U0001f470\u200d\u2640\ufe0f", "woman with veil", []string{"woman_with_veil", "bride_with_veil"}, "13.0", true}, {"\U0001f45a", "woman’s clothes", []string{"womans_clothes"}, "6.0", false}, {"\U0001f452", "woman’s hat", []string{"womans_hat"}, "6.0", false}, {"\U0001f93c\u200d\u2640\ufe0f", "women wrestling", []string{"women_wrestling"}, "9.0", false}, {"\U0001f6ba", "women’s room", []string{"womens"}, "6.0", false}, + {"\U0001fab5", "wood", []string{"wood"}, "13.0", false}, {"\U0001f974", "woozy face", []string{"woozy_face"}, "11.0", false}, {"\U0001f5fa\ufe0f", "world map", []string{"world_map"}, "7.0", false}, + {"\U0001fab1", "worm", []string{"worm"}, "13.0", false}, {"\U0001f61f", "worried face", []string{"worried"}, "6.1", false}, {"\U0001f527", "wrench", []string{"wrench"}, "6.0", false}, {"\U0001f93c", "people wrestling", []string{"wrestling"}, "11.0", false}, {"\u270d\ufe0f", "writing hand", []string{"writing_hand"}, "", true}, {"\u274c", "cross mark", []string{"x"}, "6.0", false}, + {"\U0001fa7b", "x-ray", []string{"x_ray"}, "14.0", false}, {"\U0001f9f6", "yarn", []string{"yarn"}, "11.0", false}, {"\U0001f971", "yawning face", []string{"yawning_face"}, "12.0", false}, {"\U0001f7e1", "yellow circle", []string{"yellow_circle"}, "12.0", false}, @@ -1745,86 +1856,86 @@ var GemojiData = Gemoji{ {"\U0001f9df\u200d\u2642\ufe0f", "man zombie", []string{"zombie_man"}, "11.0", false}, {"\U0001f9df\u200d\u2640\ufe0f", "woman zombie", []string{"zombie_woman"}, "11.0", false}, {"\U0001f4a4", "zzz", []string{"zzz"}, "6.0", false}, - {"\U0001f44d\U0001f3ff", "thumbs up: Dark Skin Tone", []string{"+1_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f44d\U0001f3fb", "thumbs up: Light Skin Tone", []string{"+1_Light_Skin_Tone"}, "12.0", false}, {"\U0001f44d\U0001f3fc", "thumbs up: Medium-Light Skin Tone", []string{"+1_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f44d\U0001f3fd", "thumbs up: Medium Skin Tone", []string{"+1_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f44d\U0001f3fe", "thumbs up: Medium-Dark Skin Tone", []string{"+1_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f44d\U0001f3ff", "thumbs up: Dark Skin Tone", []string{"+1_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f44e\U0001f3fe", "thumbs down: Medium-Dark Skin Tone", []string{"-1_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f44e\U0001f3ff", "thumbs down: Dark Skin Tone", []string{"-1_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f44e\U0001f3fb", "thumbs down: Light Skin Tone", []string{"-1_Light_Skin_Tone"}, "12.0", false}, {"\U0001f44e\U0001f3fc", "thumbs down: Medium-Light Skin Tone", []string{"-1_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f44e\U0001f3fd", "thumbs down: Medium Skin Tone", []string{"-1_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f44e\U0001f3fe", "thumbs down: Medium-Dark Skin Tone", []string{"-1_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb", "person: Light Skin Tone", []string{"adult_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc", "person: Medium-Light Skin Tone", []string{"adult_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd", "person: Medium Skin Tone", []string{"adult_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe", "person: Medium-Dark Skin Tone", []string{"adult_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff", "person: Dark Skin Tone", []string{"adult_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb", "person: Light Skin Tone", []string{"adult_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f47c\U0001f3fb", "baby angel: Light Skin Tone", []string{"angel_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f47c\U0001f3fc", "baby angel: Medium-Light Skin Tone", []string{"angel_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f47c\U0001f3fd", "baby angel: Medium Skin Tone", []string{"angel_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f47c\U0001f3fe", "baby angel: Medium-Dark Skin Tone", []string{"angel_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f47c\U0001f3ff", "baby angel: Dark Skin Tone", []string{"angel_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f3a8", "artist: Light Skin Tone", []string{"artist_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f47c\U0001f3fb", "baby angel: Light Skin Tone", []string{"angel_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f47c\U0001f3fc", "baby angel: Medium-Light Skin Tone", []string{"angel_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f3a8", "artist: Medium-Light Skin Tone", []string{"artist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f3a8", "artist: Medium Skin Tone", []string{"artist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f3a8", "artist: Medium-Dark Skin Tone", []string{"artist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f3a8", "artist: Dark Skin Tone", []string{"artist_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fc\u200d\U0001f680", "astronaut: Medium-Light Skin Tone", []string{"astronaut_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f3a8", "artist: Light Skin Tone", []string{"artist_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f680", "astronaut: Medium Skin Tone", []string{"astronaut_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f680", "astronaut: Medium-Dark Skin Tone", []string{"astronaut_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f680", "astronaut: Dark Skin Tone", []string{"astronaut_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f680", "astronaut: Light Skin Tone", []string{"astronaut_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f680", "astronaut: Medium-Light Skin Tone", []string{"astronaut_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f476\U0001f3fb", "baby: Light Skin Tone", []string{"baby_Light_Skin_Tone"}, "12.0", false}, {"\U0001f476\U0001f3fc", "baby: Medium-Light Skin Tone", []string{"baby_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f476\U0001f3fd", "baby: Medium Skin Tone", []string{"baby_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f476\U0001f3fe", "baby: Medium-Dark Skin Tone", []string{"baby_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f476\U0001f3ff", "baby: Dark Skin Tone", []string{"baby_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fe\u200d\U0001f9b2", "man: bald: Medium-Dark Skin Tone", []string{"bald_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3ff\u200d\U0001f9b2", "man: bald: Dark Skin Tone", []string{"bald_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f9b2", "man: bald: Light Skin Tone", []string{"bald_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f9b2", "man: bald: Medium-Light Skin Tone", []string{"bald_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f9b2", "man: bald: Medium Skin Tone", []string{"bald_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f9b2", "man: bald: Medium-Dark Skin Tone", []string{"bald_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3ff\u200d\U0001f9b2", "man: bald: Dark Skin Tone", []string{"bald_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f9b2", "woman: bald: Dark Skin Tone", []string{"bald_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\U0001f9b2", "woman: bald: Light Skin Tone", []string{"bald_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9b2", "woman: bald: Medium-Light Skin Tone", []string{"bald_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f9b2", "woman: bald: Medium Skin Tone", []string{"bald_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f9b2", "woman: bald: Medium-Dark Skin Tone", []string{"bald_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f9b2", "woman: bald: Dark Skin Tone", []string{"bald_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\U0001f9b2", "woman: bald: Light Skin Tone", []string{"bald_woman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6c0\U0001f3fe", "person taking bath: Medium-Dark Skin Tone", []string{"bath_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6c0\U0001f3ff", "person taking bath: Dark Skin Tone", []string{"bath_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6c0\U0001f3fb", "person taking bath: Light Skin Tone", []string{"bath_Light_Skin_Tone"}, "12.0", false}, {"\U0001f6c0\U0001f3fc", "person taking bath: Medium-Light Skin Tone", []string{"bath_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6c0\U0001f3fd", "person taking bath: Medium Skin Tone", []string{"bath_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f6c0\U0001f3fe", "person taking bath: Medium-Dark Skin Tone", []string{"bath_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6c0\U0001f3ff", "person taking bath: Dark Skin Tone", []string{"bath_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fb", "person: beard: Light Skin Tone", []string{"bearded_person_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fc", "person: beard: Medium-Light Skin Tone", []string{"bearded_person_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d4\U0001f3fd", "person: beard: Medium Skin Tone", []string{"bearded_person_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d4\U0001f3fe", "person: beard: Medium-Dark Skin Tone", []string{"bearded_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d4\U0001f3ff", "person: beard: Dark Skin Tone", []string{"bearded_person_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d4\U0001f3fb", "person: beard: Light Skin Tone", []string{"bearded_person_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d4\U0001f3fc", "person: beard: Medium-Light Skin Tone", []string{"bearded_person_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b4\U0001f3fb", "person biking: Light Skin Tone", []string{"bicyclist_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b4\U0001f3fc", "person biking: Medium-Light Skin Tone", []string{"bicyclist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fd", "person biking: Medium Skin Tone", []string{"bicyclist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fe", "person biking: Medium-Dark Skin Tone", []string{"bicyclist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3ff", "person biking: Dark Skin Tone", []string{"bicyclist_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6b4\U0001f3fb", "person biking: Light Skin Tone", []string{"bicyclist_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6b4\U0001f3fc", "person biking: Medium-Light Skin Tone", []string{"bicyclist_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6b4\U0001f3fe\u200d\u2642\ufe0f", "man biking: Medium-Dark Skin Tone", []string{"biking_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3ff\u200d\u2642\ufe0f", "man biking: Dark Skin Tone", []string{"biking_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fb\u200d\u2642\ufe0f", "man biking: Light Skin Tone", []string{"biking_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fc\u200d\u2642\ufe0f", "man biking: Medium-Light Skin Tone", []string{"biking_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fd\u200d\u2642\ufe0f", "man biking: Medium Skin Tone", []string{"biking_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f6b4\U0001f3fe\u200d\u2642\ufe0f", "man biking: Medium-Dark Skin Tone", []string{"biking_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fb\u200d\u2640\ufe0f", "woman biking: Light Skin Tone", []string{"biking_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fc\u200d\u2640\ufe0f", "woman biking: Medium-Light Skin Tone", []string{"biking_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fd\u200d\u2640\ufe0f", "woman biking: Medium Skin Tone", []string{"biking_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3fe\u200d\u2640\ufe0f", "woman biking: Medium-Dark Skin Tone", []string{"biking_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b4\U0001f3ff\u200d\u2640\ufe0f", "woman biking: Dark Skin Tone", []string{"biking_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f471\U0001f3ff\u200d\u2642\ufe0f", "man: blond hair: Dark Skin Tone", []string{"blond_haired_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fb\u200d\u2642\ufe0f", "man: blond hair: Light Skin Tone", []string{"blond_haired_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fc\u200d\u2642\ufe0f", "man: blond hair: Medium-Light Skin Tone", []string{"blond_haired_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fd\u200d\u2642\ufe0f", "man: blond hair: Medium Skin Tone", []string{"blond_haired_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fe\u200d\u2642\ufe0f", "man: blond hair: Medium-Dark Skin Tone", []string{"blond_haired_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f471\U0001f3ff\u200d\u2642\ufe0f", "man: blond hair: Dark Skin Tone", []string{"blond_haired_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f471\U0001f3fd", "person: blond hair: Medium Skin Tone", []string{"blond_haired_person_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f471\U0001f3fe", "person: blond hair: Medium-Dark Skin Tone", []string{"blond_haired_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3ff", "person: blond hair: Dark Skin Tone", []string{"blond_haired_person_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fb", "person: blond hair: Light Skin Tone", []string{"blond_haired_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fc", "person: blond hair: Medium-Light Skin Tone", []string{"blond_haired_person_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f471\U0001f3fd", "person: blond hair: Medium Skin Tone", []string{"blond_haired_person_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f471\U0001f3fe", "person: blond hair: Medium-Dark Skin Tone", []string{"blond_haired_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fb\u200d\u2640\ufe0f", "woman: blond hair: Light Skin Tone", []string{"blond_haired_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fc\u200d\u2640\ufe0f", "woman: blond hair: Medium-Light Skin Tone", []string{"blond_haired_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f471\U0001f3fd\u200d\u2640\ufe0f", "woman: blond hair: Medium Skin Tone", []string{"blond_haired_woman_Medium_Skin_Tone"}, "12.0", false}, @@ -1835,16 +1946,16 @@ var GemojiData = Gemoji{ {"\u26f9\U0001f3fd\ufe0f\u200d\u2642\ufe0f", "man bouncing ball: Medium Skin Tone", []string{"bouncing_ball_man_Medium_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3fe\ufe0f\u200d\u2642\ufe0f", "man bouncing ball: Medium-Dark Skin Tone", []string{"bouncing_ball_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3ff\ufe0f\u200d\u2642\ufe0f", "man bouncing ball: Dark Skin Tone", []string{"bouncing_ball_man_Dark_Skin_Tone"}, "12.0", false}, + {"\u26f9\U0001f3ff\ufe0f", "person bouncing ball: Dark Skin Tone", []string{"bouncing_ball_person_Dark_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3fb\ufe0f", "person bouncing ball: Light Skin Tone", []string{"bouncing_ball_person_Light_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3fc\ufe0f", "person bouncing ball: Medium-Light Skin Tone", []string{"bouncing_ball_person_Medium-Light_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3fd\ufe0f", "person bouncing ball: Medium Skin Tone", []string{"bouncing_ball_person_Medium_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3fe\ufe0f", "person bouncing ball: Medium-Dark Skin Tone", []string{"bouncing_ball_person_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\u26f9\U0001f3ff\ufe0f", "person bouncing ball: Dark Skin Tone", []string{"bouncing_ball_person_Dark_Skin_Tone"}, "12.0", false}, + {"\u26f9\U0001f3fc\ufe0f\u200d\u2640\ufe0f", "woman bouncing ball: Medium-Light Skin Tone", []string{"bouncing_ball_woman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\u26f9\U0001f3fd\ufe0f\u200d\u2640\ufe0f", "woman bouncing ball: Medium Skin Tone", []string{"bouncing_ball_woman_Medium_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3fe\ufe0f\u200d\u2640\ufe0f", "woman bouncing ball: Medium-Dark Skin Tone", []string{"bouncing_ball_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3ff\ufe0f\u200d\u2640\ufe0f", "woman bouncing ball: Dark Skin Tone", []string{"bouncing_ball_woman_Dark_Skin_Tone"}, "12.0", false}, {"\u26f9\U0001f3fb\ufe0f\u200d\u2640\ufe0f", "woman bouncing ball: Light Skin Tone", []string{"bouncing_ball_woman_Light_Skin_Tone"}, "12.0", false}, - {"\u26f9\U0001f3fc\ufe0f\u200d\u2640\ufe0f", "woman bouncing ball: Medium-Light Skin Tone", []string{"bouncing_ball_woman_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\u26f9\U0001f3fd\ufe0f\u200d\u2640\ufe0f", "woman bouncing ball: Medium Skin Tone", []string{"bouncing_ball_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fd", "person bowing: Medium Skin Tone", []string{"bow_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fe", "person bowing: Medium-Dark Skin Tone", []string{"bow_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3ff", "person bowing: Dark Skin Tone", []string{"bow_Dark_Skin_Tone"}, "12.0", false}, @@ -1855,16 +1966,16 @@ var GemojiData = Gemoji{ {"\U0001f647\U0001f3fc\u200d\u2642\ufe0f", "man bowing: Medium-Light Skin Tone", []string{"bowing_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fd\u200d\u2642\ufe0f", "man bowing: Medium Skin Tone", []string{"bowing_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fe\u200d\u2642\ufe0f", "man bowing: Medium-Dark Skin Tone", []string{"bowing_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f647\U0001f3ff\u200d\u2640\ufe0f", "woman bowing: Dark Skin Tone", []string{"bowing_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fb\u200d\u2640\ufe0f", "woman bowing: Light Skin Tone", []string{"bowing_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fc\u200d\u2640\ufe0f", "woman bowing: Medium-Light Skin Tone", []string{"bowing_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fd\u200d\u2640\ufe0f", "woman bowing: Medium Skin Tone", []string{"bowing_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f647\U0001f3fe\u200d\u2640\ufe0f", "woman bowing: Medium-Dark Skin Tone", []string{"bowing_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f647\U0001f3ff\u200d\u2640\ufe0f", "woman bowing: Dark Skin Tone", []string{"bowing_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f466\U0001f3ff", "boy: Dark Skin Tone", []string{"boy_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f466\U0001f3fb", "boy: Light Skin Tone", []string{"boy_Light_Skin_Tone"}, "12.0", false}, {"\U0001f466\U0001f3fc", "boy: Medium-Light Skin Tone", []string{"boy_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f466\U0001f3fd", "boy: Medium Skin Tone", []string{"boy_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f466\U0001f3fe", "boy: Medium-Dark Skin Tone", []string{"boy_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f466\U0001f3ff", "boy: Dark Skin Tone", []string{"boy_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f931\U0001f3fb", "breast-feeding: Light Skin Tone", []string{"breast_feeding_Light_Skin_Tone"}, "12.0", false}, {"\U0001f931\U0001f3fc", "breast-feeding: Medium-Light Skin Tone", []string{"breast_feeding_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f931\U0001f3fd", "breast-feeding: Medium Skin Tone", []string{"breast_feeding_Medium_Skin_Tone"}, "12.0", false}, @@ -1875,236 +1986,236 @@ var GemojiData = Gemoji{ {"\U0001f574\U0001f3fd\ufe0f", "person in suit levitating: Medium Skin Tone", []string{"business_suit_levitating_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f574\U0001f3fe\ufe0f", "person in suit levitating: Medium-Dark Skin Tone", []string{"business_suit_levitating_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f574\U0001f3ff\ufe0f", "person in suit levitating: Dark Skin Tone", []string{"business_suit_levitating_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f919\U0001f3fb", "call me hand: Light Skin Tone", []string{"call_me_hand_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f919\U0001f3fc", "call me hand: Medium-Light Skin Tone", []string{"call_me_hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f919\U0001f3fd", "call me hand: Medium Skin Tone", []string{"call_me_hand_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f919\U0001f3fe", "call me hand: Medium-Dark Skin Tone", []string{"call_me_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f919\U0001f3ff", "call me hand: Dark Skin Tone", []string{"call_me_hand_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f938\U0001f3ff", "person cartwheeling: Dark Skin Tone", []string{"cartwheeling_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f919\U0001f3fb", "call me hand: Light Skin Tone", []string{"call_me_hand_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f919\U0001f3fc", "call me hand: Medium-Light Skin Tone", []string{"call_me_hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fb", "person cartwheeling: Light Skin Tone", []string{"cartwheeling_Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fc", "person cartwheeling: Medium-Light Skin Tone", []string{"cartwheeling_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fd", "person cartwheeling: Medium Skin Tone", []string{"cartwheeling_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fe", "person cartwheeling: Medium-Dark Skin Tone", []string{"cartwheeling_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f938\U0001f3ff", "person cartwheeling: Dark Skin Tone", []string{"cartwheeling_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d2\U0001f3fb", "child: Light Skin Tone", []string{"child_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d2\U0001f3fc", "child: Medium-Light Skin Tone", []string{"child_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d2\U0001f3fd", "child: Medium Skin Tone", []string{"child_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d2\U0001f3fe", "child: Medium-Dark Skin Tone", []string{"child_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d2\U0001f3ff", "child: Dark Skin Tone", []string{"child_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f44f\U0001f3fb", "clapping hands: Light Skin Tone", []string{"clap_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f44f\U0001f3fc", "clapping hands: Medium-Light Skin Tone", []string{"clap_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f44f\U0001f3fd", "clapping hands: Medium Skin Tone", []string{"clap_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f44f\U0001f3fe", "clapping hands: Medium-Dark Skin Tone", []string{"clap_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f44f\U0001f3ff", "clapping hands: Dark Skin Tone", []string{"clap_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f44f\U0001f3fb", "clapping hands: Light Skin Tone", []string{"clap_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f44f\U0001f3fc", "clapping hands: Medium-Light Skin Tone", []string{"clap_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fb", "person climbing: Light Skin Tone", []string{"climbing_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fc", "person climbing: Medium-Light Skin Tone", []string{"climbing_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fd", "person climbing: Medium Skin Tone", []string{"climbing_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fe", "person climbing: Medium-Dark Skin Tone", []string{"climbing_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3ff", "person climbing: Dark Skin Tone", []string{"climbing_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d7\U0001f3ff\u200d\u2642\ufe0f", "man climbing: Dark Skin Tone", []string{"climbing_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fb\u200d\u2642\ufe0f", "man climbing: Light Skin Tone", []string{"climbing_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fc\u200d\u2642\ufe0f", "man climbing: Medium-Light Skin Tone", []string{"climbing_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fd\u200d\u2642\ufe0f", "man climbing: Medium Skin Tone", []string{"climbing_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fe\u200d\u2642\ufe0f", "man climbing: Medium-Dark Skin Tone", []string{"climbing_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d7\U0001f3ff\u200d\u2642\ufe0f", "man climbing: Dark Skin Tone", []string{"climbing_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d7\U0001f3ff\u200d\u2640\ufe0f", "woman climbing: Dark Skin Tone", []string{"climbing_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fb\u200d\u2640\ufe0f", "woman climbing: Light Skin Tone", []string{"climbing_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fc\u200d\u2640\ufe0f", "woman climbing: Medium-Light Skin Tone", []string{"climbing_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fd\u200d\u2640\ufe0f", "woman climbing: Medium Skin Tone", []string{"climbing_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d7\U0001f3fe\u200d\u2640\ufe0f", "woman climbing: Medium-Dark Skin Tone", []string{"climbing_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d7\U0001f3ff\u200d\u2640\ufe0f", "woman climbing: Dark Skin Tone", []string{"climbing_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fd", "construction worker: Medium Skin Tone", []string{"construction_worker_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fe", "construction worker: Medium-Dark Skin Tone", []string{"construction_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3ff", "construction worker: Dark Skin Tone", []string{"construction_worker_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fb", "construction worker: Light Skin Tone", []string{"construction_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fc", "construction worker: Medium-Light Skin Tone", []string{"construction_worker_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f477\U0001f3ff\u200d\u2642\ufe0f", "man construction worker: Dark Skin Tone", []string{"construction_worker_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fb\u200d\u2642\ufe0f", "man construction worker: Light Skin Tone", []string{"construction_worker_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fc\u200d\u2642\ufe0f", "man construction worker: Medium-Light Skin Tone", []string{"construction_worker_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fd\u200d\u2642\ufe0f", "man construction worker: Medium Skin Tone", []string{"construction_worker_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fe\u200d\u2642\ufe0f", "man construction worker: Medium-Dark Skin Tone", []string{"construction_worker_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f477\U0001f3ff\u200d\u2640\ufe0f", "woman construction worker: Dark Skin Tone", []string{"construction_worker_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f477\U0001f3ff\u200d\u2642\ufe0f", "man construction worker: Dark Skin Tone", []string{"construction_worker_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fb\u200d\u2640\ufe0f", "woman construction worker: Light Skin Tone", []string{"construction_worker_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fc\u200d\u2640\ufe0f", "woman construction worker: Medium-Light Skin Tone", []string{"construction_worker_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fd\u200d\u2640\ufe0f", "woman construction worker: Medium Skin Tone", []string{"construction_worker_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f477\U0001f3fe\u200d\u2640\ufe0f", "woman construction worker: Medium-Dark Skin Tone", []string{"construction_worker_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f373", "cook: Light Skin Tone", []string{"cook_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f477\U0001f3ff\u200d\u2640\ufe0f", "woman construction worker: Dark Skin Tone", []string{"construction_worker_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f373", "cook: Medium-Light Skin Tone", []string{"cook_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f373", "cook: Medium Skin Tone", []string{"cook_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f373", "cook: Medium-Dark Skin Tone", []string{"cook_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f373", "cook: Dark Skin Tone", []string{"cook_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f373", "cook: Light Skin Tone", []string{"cook_Light_Skin_Tone"}, "12.0", false}, {"\U0001f46b\U0001f3fb", "woman and man holding hands: Light Skin Tone", []string{"couple_Light_Skin_Tone"}, "12.0", false}, {"\U0001f46b\U0001f3fc", "woman and man holding hands: Medium-Light Skin Tone", []string{"couple_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f46b\U0001f3fd", "woman and man holding hands: Medium Skin Tone", []string{"couple_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f46b\U0001f3fe", "woman and man holding hands: Medium-Dark Skin Tone", []string{"couple_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46b\U0001f3ff", "woman and man holding hands: Dark Skin Tone", []string{"couple_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f491\U0001f3fb", "couple with heart: Light Skin Tone", []string{"couple_with_heart_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f491\U0001f3fc", "couple with heart: Medium-Light Skin Tone", []string{"couple_with_heart_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f491\U0001f3fd", "couple with heart: Medium Skin Tone", []string{"couple_with_heart_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f491\U0001f3fe", "couple with heart: Medium-Dark Skin Tone", []string{"couple_with_heart_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f491\U0001f3ff", "couple with heart: Dark Skin Tone", []string{"couple_with_heart_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f491\U0001f3fb", "couple with heart: Light Skin Tone", []string{"couple_with_heart_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f491\U0001f3fc", "couple with heart: Medium-Light Skin Tone", []string{"couple_with_heart_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fd\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: man, man: Medium Skin Tone", []string{"couple_with_heart_man_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: man, man: Medium-Dark Skin Tone", []string{"couple_with_heart_man_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: man, man: Dark Skin Tone", []string{"couple_with_heart_man_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: man, man: Light Skin Tone", []string{"couple_with_heart_man_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: man, man: Medium-Light Skin Tone", []string{"couple_with_heart_man_man_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: woman, man: Light Skin Tone", []string{"couple_with_heart_woman_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fd\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: man, man: Medium Skin Tone", []string{"couple_with_heart_man_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: woman, man: Medium-Light Skin Tone", []string{"couple_with_heart_woman_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: woman, man: Medium Skin Tone", []string{"couple_with_heart_woman_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: woman, man: Medium-Dark Skin Tone", []string{"couple_with_heart_woman_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: woman, man: Dark Skin Tone", []string{"couple_with_heart_woman_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f468", "couple with heart: woman, man: Light Skin Tone", []string{"couple_with_heart_woman_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f469", "couple with heart: woman, woman: Light Skin Tone", []string{"couple_with_heart_woman_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\u2764\ufe0f\u200d\U0001f469", "couple with heart: woman, woman: Medium-Light Skin Tone", []string{"couple_with_heart_woman_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\u2764\ufe0f\u200d\U0001f469", "couple with heart: woman, woman: Medium Skin Tone", []string{"couple_with_heart_woman_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\u2764\ufe0f\u200d\U0001f469", "couple with heart: woman, woman: Medium-Dark Skin Tone", []string{"couple_with_heart_woman_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f469", "couple with heart: woman, woman: Dark Skin Tone", []string{"couple_with_heart_woman_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f469", "couple with heart: woman, woman: Light Skin Tone", []string{"couple_with_heart_woman_woman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fc\u200d\u2764\ufe0f\u200d\U0001f469", "couple with heart: woman, woman: Medium-Light Skin Tone", []string{"couple_with_heart_woman_woman_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f48f\U0001f3fe", "kiss: Medium-Dark Skin Tone", []string{"couplekiss_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f48f\U0001f3ff", "kiss: Dark Skin Tone", []string{"couplekiss_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f48f\U0001f3fb", "kiss: Light Skin Tone", []string{"couplekiss_Light_Skin_Tone"}, "12.0", false}, {"\U0001f48f\U0001f3fc", "kiss: Medium-Light Skin Tone", []string{"couplekiss_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f48f\U0001f3fd", "kiss: Medium Skin Tone", []string{"couplekiss_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: man, man: Dark Skin Tone", []string{"couplekiss_man_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f48f\U0001f3fe", "kiss: Medium-Dark Skin Tone", []string{"couplekiss_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f48f\U0001f3ff", "kiss: Dark Skin Tone", []string{"couplekiss_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: man, man: Light Skin Tone", []string{"couplekiss_man_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: man, man: Medium-Light Skin Tone", []string{"couplekiss_man_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: man, man: Medium Skin Tone", []string{"couplekiss_man_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: man, man: Medium-Dark Skin Tone", []string{"couplekiss_man_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: woman, man: Light Skin Tone", []string{"couplekiss_man_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: man, man: Dark Skin Tone", []string{"couplekiss_man_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: woman, man: Medium-Light Skin Tone", []string{"couplekiss_man_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: woman, man: Medium Skin Tone", []string{"couplekiss_man_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: woman, man: Medium-Dark Skin Tone", []string{"couplekiss_man_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: woman, man: Dark Skin Tone", []string{"couplekiss_man_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", "kiss: woman, man: Light Skin Tone", []string{"couplekiss_man_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469", "kiss: woman, woman: Dark Skin Tone", []string{"couplekiss_woman_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469", "kiss: woman, woman: Light Skin Tone", []string{"couplekiss_woman_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469", "kiss: woman, woman: Medium-Light Skin Tone", []string{"couplekiss_woman_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469", "kiss: woman, woman: Medium Skin Tone", []string{"couplekiss_woman_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469", "kiss: woman, woman: Medium-Dark Skin Tone", []string{"couplekiss_woman_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469", "kiss: woman, woman: Dark Skin Tone", []string{"couplekiss_woman_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f91e\U0001f3fe", "crossed fingers: Medium-Dark Skin Tone", []string{"crossed_fingers_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f91e\U0001f3ff", "crossed fingers: Dark Skin Tone", []string{"crossed_fingers_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f91e\U0001f3fb", "crossed fingers: Light Skin Tone", []string{"crossed_fingers_Light_Skin_Tone"}, "12.0", false}, {"\U0001f91e\U0001f3fc", "crossed fingers: Medium-Light Skin Tone", []string{"crossed_fingers_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f91e\U0001f3fd", "crossed fingers: Medium Skin Tone", []string{"crossed_fingers_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f91e\U0001f3fe", "crossed fingers: Medium-Dark Skin Tone", []string{"crossed_fingers_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f91e\U0001f3ff", "crossed fingers: Dark Skin Tone", []string{"crossed_fingers_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fd\u200d\U0001f9b1", "man: curly hair: Medium Skin Tone", []string{"curly_haired_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f9b1", "man: curly hair: Medium-Dark Skin Tone", []string{"curly_haired_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f9b1", "man: curly hair: Dark Skin Tone", []string{"curly_haired_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f9b1", "man: curly hair: Light Skin Tone", []string{"curly_haired_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f9b1", "man: curly hair: Medium-Light Skin Tone", []string{"curly_haired_man_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fd\u200d\U0001f9b1", "man: curly hair: Medium Skin Tone", []string{"curly_haired_man_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fe\u200d\U0001f9b1", "man: curly hair: Medium-Dark Skin Tone", []string{"curly_haired_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fd\u200d\U0001f9b1", "woman: curly hair: Medium Skin Tone", []string{"curly_haired_woman_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f9b1", "woman: curly hair: Medium-Dark Skin Tone", []string{"curly_haired_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f9b1", "woman: curly hair: Dark Skin Tone", []string{"curly_haired_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f9b1", "woman: curly hair: Light Skin Tone", []string{"curly_haired_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9b1", "woman: curly hair: Medium-Light Skin Tone", []string{"curly_haired_woman_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fd\u200d\U0001f9b1", "woman: curly hair: Medium Skin Tone", []string{"curly_haired_woman_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fe\u200d\U0001f9b1", "woman: curly hair: Medium-Dark Skin Tone", []string{"curly_haired_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cf\U0001f3ff\u200d\u2642\ufe0f", "deaf man: Dark Skin Tone", []string{"deaf_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cf\U0001f3fb\u200d\u2642\ufe0f", "deaf man: Light Skin Tone", []string{"deaf_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fc\u200d\u2642\ufe0f", "deaf man: Medium-Light Skin Tone", []string{"deaf_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fd\u200d\u2642\ufe0f", "deaf man: Medium Skin Tone", []string{"deaf_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fe\u200d\u2642\ufe0f", "deaf man: Medium-Dark Skin Tone", []string{"deaf_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cf\U0001f3fb", "deaf person: Light Skin Tone", []string{"deaf_person_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9cf\U0001f3ff\u200d\u2642\ufe0f", "deaf man: Dark Skin Tone", []string{"deaf_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9cf\U0001f3fb\u200d\u2642\ufe0f", "deaf man: Light Skin Tone", []string{"deaf_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fc", "deaf person: Medium-Light Skin Tone", []string{"deaf_person_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fd", "deaf person: Medium Skin Tone", []string{"deaf_person_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fe", "deaf person: Medium-Dark Skin Tone", []string{"deaf_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3ff", "deaf person: Dark Skin Tone", []string{"deaf_person_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cf\U0001f3fe\u200d\u2640\ufe0f", "deaf woman: Medium-Dark Skin Tone", []string{"deaf_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cf\U0001f3ff\u200d\u2640\ufe0f", "deaf woman: Dark Skin Tone", []string{"deaf_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9cf\U0001f3fb", "deaf person: Light Skin Tone", []string{"deaf_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fb\u200d\u2640\ufe0f", "deaf woman: Light Skin Tone", []string{"deaf_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fc\u200d\u2640\ufe0f", "deaf woman: Medium-Light Skin Tone", []string{"deaf_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cf\U0001f3fd\u200d\u2640\ufe0f", "deaf woman: Medium Skin Tone", []string{"deaf_woman_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9cf\U0001f3fe\u200d\u2640\ufe0f", "deaf woman: Medium-Dark Skin Tone", []string{"deaf_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9cf\U0001f3ff\u200d\u2640\ufe0f", "deaf woman: Dark Skin Tone", []string{"deaf_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fb\ufe0f", "detective: Light Skin Tone", []string{"detective_Light_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fc\ufe0f", "detective: Medium-Light Skin Tone", []string{"detective_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fd\ufe0f", "detective: Medium Skin Tone", []string{"detective_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fe\ufe0f", "detective: Medium-Dark Skin Tone", []string{"detective_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3ff\ufe0f", "detective: Dark Skin Tone", []string{"detective_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f442\U0001f3fe", "ear: Medium-Dark Skin Tone", []string{"ear_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f442\U0001f3ff", "ear: Dark Skin Tone", []string{"ear_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f442\U0001f3fb", "ear: Light Skin Tone", []string{"ear_Light_Skin_Tone"}, "12.0", false}, {"\U0001f442\U0001f3fc", "ear: Medium-Light Skin Tone", []string{"ear_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f442\U0001f3fd", "ear: Medium Skin Tone", []string{"ear_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f442\U0001f3fe", "ear: Medium-Dark Skin Tone", []string{"ear_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f442\U0001f3ff", "ear: Dark Skin Tone", []string{"ear_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9bb\U0001f3fe", "ear with hearing aid: Medium-Dark Skin Tone", []string{"ear_with_hearing_aid_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9bb\U0001f3ff", "ear with hearing aid: Dark Skin Tone", []string{"ear_with_hearing_aid_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9bb\U0001f3fb", "ear with hearing aid: Light Skin Tone", []string{"ear_with_hearing_aid_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9bb\U0001f3fc", "ear with hearing aid: Medium-Light Skin Tone", []string{"ear_with_hearing_aid_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9bb\U0001f3fd", "ear with hearing aid: Medium Skin Tone", []string{"ear_with_hearing_aid_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9dd\U0001f3fe", "elf: Medium-Dark Skin Tone", []string{"elf_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9dd\U0001f3ff", "elf: Dark Skin Tone", []string{"elf_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fb", "elf: Light Skin Tone", []string{"elf_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fc", "elf: Medium-Light Skin Tone", []string{"elf_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fd", "elf: Medium Skin Tone", []string{"elf_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9dd\U0001f3fe", "elf: Medium-Dark Skin Tone", []string{"elf_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9dd\U0001f3ff", "elf: Dark Skin Tone", []string{"elf_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fb\u200d\u2642\ufe0f", "man elf: Light Skin Tone", []string{"elf_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fc\u200d\u2642\ufe0f", "man elf: Medium-Light Skin Tone", []string{"elf_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fd\u200d\u2642\ufe0f", "man elf: Medium Skin Tone", []string{"elf_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fe\u200d\u2642\ufe0f", "man elf: Medium-Dark Skin Tone", []string{"elf_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3ff\u200d\u2642\ufe0f", "man elf: Dark Skin Tone", []string{"elf_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9dd\U0001f3fe\u200d\u2640\ufe0f", "woman elf: Medium-Dark Skin Tone", []string{"elf_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9dd\U0001f3ff\u200d\u2640\ufe0f", "woman elf: Dark Skin Tone", []string{"elf_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fb\u200d\u2640\ufe0f", "woman elf: Light Skin Tone", []string{"elf_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fc\u200d\u2640\ufe0f", "woman elf: Medium-Light Skin Tone", []string{"elf_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dd\U0001f3fd\u200d\u2640\ufe0f", "woman elf: Medium Skin Tone", []string{"elf_woman_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9dd\U0001f3fe\u200d\u2640\ufe0f", "woman elf: Medium-Dark Skin Tone", []string{"elf_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9dd\U0001f3ff\u200d\u2640\ufe0f", "woman elf: Dark Skin Tone", []string{"elf_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f926\U0001f3ff", "person facepalming: Dark Skin Tone", []string{"facepalm_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fb", "person facepalming: Light Skin Tone", []string{"facepalm_Light_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fc", "person facepalming: Medium-Light Skin Tone", []string{"facepalm_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fd", "person facepalming: Medium Skin Tone", []string{"facepalm_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fe", "person facepalming: Medium-Dark Skin Tone", []string{"facepalm_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f926\U0001f3ff", "person facepalming: Dark Skin Tone", []string{"facepalm_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f3ed", "factory worker: Dark Skin Tone", []string{"factory_worker_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f3ed", "factory worker: Light Skin Tone", []string{"factory_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f3ed", "factory worker: Medium-Light Skin Tone", []string{"factory_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f3ed", "factory worker: Medium Skin Tone", []string{"factory_worker_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f3ed", "factory worker: Medium-Dark Skin Tone", []string{"factory_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f3ed", "factory worker: Dark Skin Tone", []string{"factory_worker_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9da\U0001f3fd", "fairy: Medium Skin Tone", []string{"fairy_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fe", "fairy: Medium-Dark Skin Tone", []string{"fairy_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3ff", "fairy: Dark Skin Tone", []string{"fairy_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fb", "fairy: Light Skin Tone", []string{"fairy_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fc", "fairy: Medium-Light Skin Tone", []string{"fairy_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9da\U0001f3fd", "fairy: Medium Skin Tone", []string{"fairy_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fb\u200d\u2642\ufe0f", "man fairy: Light Skin Tone", []string{"fairy_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fc\u200d\u2642\ufe0f", "man fairy: Medium-Light Skin Tone", []string{"fairy_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fd\u200d\u2642\ufe0f", "man fairy: Medium Skin Tone", []string{"fairy_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fe\u200d\u2642\ufe0f", "man fairy: Medium-Dark Skin Tone", []string{"fairy_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3ff\u200d\u2642\ufe0f", "man fairy: Dark Skin Tone", []string{"fairy_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9da\U0001f3ff\u200d\u2640\ufe0f", "woman fairy: Dark Skin Tone", []string{"fairy_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fb\u200d\u2640\ufe0f", "woman fairy: Light Skin Tone", []string{"fairy_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fc\u200d\u2640\ufe0f", "woman fairy: Medium-Light Skin Tone", []string{"fairy_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fd\u200d\u2640\ufe0f", "woman fairy: Medium Skin Tone", []string{"fairy_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9da\U0001f3fe\u200d\u2640\ufe0f", "woman fairy: Medium-Dark Skin Tone", []string{"fairy_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9da\U0001f3ff\u200d\u2640\ufe0f", "woman fairy: Dark Skin Tone", []string{"fairy_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f33e", "farmer: Light Skin Tone", []string{"farmer_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f33e", "farmer: Medium-Light Skin Tone", []string{"farmer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f33e", "farmer: Medium Skin Tone", []string{"farmer_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f33e", "farmer: Medium-Dark Skin Tone", []string{"farmer_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f33e", "farmer: Dark Skin Tone", []string{"farmer_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f33e", "farmer: Light Skin Tone", []string{"farmer_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fc\u200d\U0001f33e", "farmer: Medium-Light Skin Tone", []string{"farmer_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f575\U0001f3ff\ufe0f\u200d\u2640\ufe0f", "woman detective: Dark Skin Tone", []string{"female_detective_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fb\ufe0f\u200d\u2640\ufe0f", "woman detective: Light Skin Tone", []string{"female_detective_Light_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fc\ufe0f\u200d\u2640\ufe0f", "woman detective: Medium-Light Skin Tone", []string{"female_detective_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fd\ufe0f\u200d\u2640\ufe0f", "woman detective: Medium Skin Tone", []string{"female_detective_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fe\ufe0f\u200d\u2640\ufe0f", "woman detective: Medium-Dark Skin Tone", []string{"female_detective_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f575\U0001f3ff\ufe0f\u200d\u2640\ufe0f", "woman detective: Dark Skin Tone", []string{"female_detective_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f692", "firefighter: Light Skin Tone", []string{"firefighter_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f692", "firefighter: Medium-Light Skin Tone", []string{"firefighter_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f692", "firefighter: Medium Skin Tone", []string{"firefighter_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f692", "firefighter: Medium-Dark Skin Tone", []string{"firefighter_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f692", "firefighter: Dark Skin Tone", []string{"firefighter_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f692", "firefighter: Light Skin Tone", []string{"firefighter_Light_Skin_Tone"}, "12.0", false}, {"\U0001f91b\U0001f3fb", "left-facing fist: Light Skin Tone", []string{"fist_left_Light_Skin_Tone"}, "12.0", false}, {"\U0001f91b\U0001f3fc", "left-facing fist: Medium-Light Skin Tone", []string{"fist_left_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f91b\U0001f3fd", "left-facing fist: Medium Skin Tone", []string{"fist_left_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f91b\U0001f3fe", "left-facing fist: Medium-Dark Skin Tone", []string{"fist_left_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f91b\U0001f3ff", "left-facing fist: Dark Skin Tone", []string{"fist_left_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f44a\U0001f3fb", "oncoming fist: Light Skin Tone", []string{"fist_oncoming_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f44a\U0001f3fc", "oncoming fist: Medium-Light Skin Tone", []string{"fist_oncoming_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f44a\U0001f3fd", "oncoming fist: Medium Skin Tone", []string{"fist_oncoming_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f44a\U0001f3fe", "oncoming fist: Medium-Dark Skin Tone", []string{"fist_oncoming_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f44a\U0001f3ff", "oncoming fist: Dark Skin Tone", []string{"fist_oncoming_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f44a\U0001f3fb", "oncoming fist: Light Skin Tone", []string{"fist_oncoming_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f44a\U0001f3fc", "oncoming fist: Medium-Light Skin Tone", []string{"fist_oncoming_Medium-Light_Skin_Tone"}, "12.0", false}, {"\u270a\U0001f3fb", "raised fist: Light Skin Tone", []string{"fist_raised_Light_Skin_Tone"}, "12.0", false}, {"\u270a\U0001f3fc", "raised fist: Medium-Light Skin Tone", []string{"fist_raised_Medium-Light_Skin_Tone"}, "12.0", false}, {"\u270a\U0001f3fd", "raised fist: Medium Skin Tone", []string{"fist_raised_Medium_Skin_Tone"}, "12.0", false}, {"\u270a\U0001f3fe", "raised fist: Medium-Dark Skin Tone", []string{"fist_raised_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\u270a\U0001f3ff", "raised fist: Dark Skin Tone", []string{"fist_raised_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f91c\U0001f3fe", "right-facing fist: Medium-Dark Skin Tone", []string{"fist_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f91c\U0001f3ff", "right-facing fist: Dark Skin Tone", []string{"fist_right_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f91c\U0001f3fb", "right-facing fist: Light Skin Tone", []string{"fist_right_Light_Skin_Tone"}, "12.0", false}, {"\U0001f91c\U0001f3fc", "right-facing fist: Medium-Light Skin Tone", []string{"fist_right_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f91c\U0001f3fd", "right-facing fist: Medium Skin Tone", []string{"fist_right_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f91c\U0001f3fe", "right-facing fist: Medium-Dark Skin Tone", []string{"fist_right_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f91c\U0001f3ff", "right-facing fist: Dark Skin Tone", []string{"fist_right_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9b6\U0001f3fc", "foot: Medium-Light Skin Tone", []string{"foot_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9b6\U0001f3fd", "foot: Medium Skin Tone", []string{"foot_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9b6\U0001f3fe", "foot: Medium-Dark Skin Tone", []string{"foot_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b6\U0001f3ff", "foot: Dark Skin Tone", []string{"foot_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b6\U0001f3fb", "foot: Light Skin Tone", []string{"foot_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f64d\U0001f3fd\u200d\u2642\ufe0f", "man frowning: Medium Skin Tone", []string{"frowning_man_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f64d\U0001f3fe\u200d\u2642\ufe0f", "man frowning: Medium-Dark Skin Tone", []string{"frowning_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9b6\U0001f3fc", "foot: Medium-Light Skin Tone", []string{"foot_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9b6\U0001f3fd", "foot: Medium Skin Tone", []string{"foot_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3ff\u200d\u2642\ufe0f", "man frowning: Dark Skin Tone", []string{"frowning_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3fb\u200d\u2642\ufe0f", "man frowning: Light Skin Tone", []string{"frowning_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3fc\u200d\u2642\ufe0f", "man frowning: Medium-Light Skin Tone", []string{"frowning_man_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f64d\U0001f3fd\u200d\u2642\ufe0f", "man frowning: Medium Skin Tone", []string{"frowning_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f64d\U0001f3fe\u200d\u2642\ufe0f", "man frowning: Medium-Dark Skin Tone", []string{"frowning_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3fb", "person frowning: Light Skin Tone", []string{"frowning_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3fc", "person frowning: Medium-Light Skin Tone", []string{"frowning_person_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3fd", "person frowning: Medium Skin Tone", []string{"frowning_person_Medium_Skin_Tone"}, "12.0", false}, @@ -2115,86 +2226,106 @@ var GemojiData = Gemoji{ {"\U0001f64d\U0001f3fd\u200d\u2640\ufe0f", "woman frowning: Medium Skin Tone", []string{"frowning_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3fe\u200d\u2640\ufe0f", "woman frowning: Medium-Dark Skin Tone", []string{"frowning_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64d\U0001f3ff\u200d\u2640\ufe0f", "woman frowning: Dark Skin Tone", []string{"frowning_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f467\U0001f3fb", "girl: Light Skin Tone", []string{"girl_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f467\U0001f3fc", "girl: Medium-Light Skin Tone", []string{"girl_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f467\U0001f3fd", "girl: Medium Skin Tone", []string{"girl_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f467\U0001f3fe", "girl: Medium-Dark Skin Tone", []string{"girl_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f467\U0001f3ff", "girl: Dark Skin Tone", []string{"girl_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f467\U0001f3fb", "girl: Light Skin Tone", []string{"girl_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f467\U0001f3fc", "girl: Medium-Light Skin Tone", []string{"girl_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f3cc\U0001f3ff\ufe0f", "person golfing: Dark Skin Tone", []string{"golfing_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fb\ufe0f", "person golfing: Light Skin Tone", []string{"golfing_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fc\ufe0f", "person golfing: Medium-Light Skin Tone", []string{"golfing_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fd\ufe0f", "person golfing: Medium Skin Tone", []string{"golfing_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fe\ufe0f", "person golfing: Medium-Dark Skin Tone", []string{"golfing_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3cc\U0001f3fb\ufe0f\u200d\u2642\ufe0f", "man golfing: Light Skin Tone", []string{"golfing_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3cc\U0001f3ff\ufe0f", "person golfing: Dark Skin Tone", []string{"golfing_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fc\ufe0f\u200d\u2642\ufe0f", "man golfing: Medium-Light Skin Tone", []string{"golfing_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fd\ufe0f\u200d\u2642\ufe0f", "man golfing: Medium Skin Tone", []string{"golfing_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fe\ufe0f\u200d\u2642\ufe0f", "man golfing: Medium-Dark Skin Tone", []string{"golfing_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3ff\ufe0f\u200d\u2642\ufe0f", "man golfing: Dark Skin Tone", []string{"golfing_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3cc\U0001f3fb\ufe0f\u200d\u2642\ufe0f", "man golfing: Light Skin Tone", []string{"golfing_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3cc\U0001f3fc\ufe0f\u200d\u2640\ufe0f", "woman golfing: Medium-Light Skin Tone", []string{"golfing_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fd\ufe0f\u200d\u2640\ufe0f", "woman golfing: Medium Skin Tone", []string{"golfing_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fe\ufe0f\u200d\u2640\ufe0f", "woman golfing: Medium-Dark Skin Tone", []string{"golfing_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3ff\ufe0f\u200d\u2640\ufe0f", "woman golfing: Dark Skin Tone", []string{"golfing_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cc\U0001f3fb\ufe0f\u200d\u2640\ufe0f", "woman golfing: Light Skin Tone", []string{"golfing_woman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f3cc\U0001f3fc\ufe0f\u200d\u2640\ufe0f", "woman golfing: Medium-Light Skin Tone", []string{"golfing_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fb", "guard: Light Skin Tone", []string{"guard_Light_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fc", "guard: Medium-Light Skin Tone", []string{"guard_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fd", "guard: Medium Skin Tone", []string{"guard_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fe", "guard: Medium-Dark Skin Tone", []string{"guard_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3ff", "guard: Dark Skin Tone", []string{"guard_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f482\U0001f3fb\u200d\u2642\ufe0f", "man guard: Light Skin Tone", []string{"guardsman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f482\U0001f3fc\u200d\u2642\ufe0f", "man guard: Medium-Light Skin Tone", []string{"guardsman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fd\u200d\u2642\ufe0f", "man guard: Medium Skin Tone", []string{"guardsman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fe\u200d\u2642\ufe0f", "man guard: Medium-Dark Skin Tone", []string{"guardsman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3ff\u200d\u2642\ufe0f", "man guard: Dark Skin Tone", []string{"guardsman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f482\U0001f3fb\u200d\u2642\ufe0f", "man guard: Light Skin Tone", []string{"guardsman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f482\U0001f3fc\u200d\u2642\ufe0f", "man guard: Medium-Light Skin Tone", []string{"guardsman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fb\u200d\u2640\ufe0f", "woman guard: Light Skin Tone", []string{"guardswoman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fc\u200d\u2640\ufe0f", "woman guard: Medium-Light Skin Tone", []string{"guardswoman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fd\u200d\u2640\ufe0f", "woman guard: Medium Skin Tone", []string{"guardswoman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3fe\u200d\u2640\ufe0f", "woman guard: Medium-Dark Skin Tone", []string{"guardswoman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f482\U0001f3ff\u200d\u2640\ufe0f", "woman guard: Dark Skin Tone", []string{"guardswoman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f487\U0001f3fc", "person getting haircut: Medium-Light Skin Tone", []string{"haircut_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f487\U0001f3fd", "person getting haircut: Medium Skin Tone", []string{"haircut_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fe", "person getting haircut: Medium-Dark Skin Tone", []string{"haircut_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3ff", "person getting haircut: Dark Skin Tone", []string{"haircut_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fb", "person getting haircut: Light Skin Tone", []string{"haircut_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f487\U0001f3fc", "person getting haircut: Medium-Light Skin Tone", []string{"haircut_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f487\U0001f3fd", "person getting haircut: Medium Skin Tone", []string{"haircut_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f487\U0001f3fc\u200d\u2642\ufe0f", "man getting haircut: Medium-Light Skin Tone", []string{"haircut_man_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f487\U0001f3fd\u200d\u2642\ufe0f", "man getting haircut: Medium Skin Tone", []string{"haircut_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fe\u200d\u2642\ufe0f", "man getting haircut: Medium-Dark Skin Tone", []string{"haircut_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3ff\u200d\u2642\ufe0f", "man getting haircut: Dark Skin Tone", []string{"haircut_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fb\u200d\u2642\ufe0f", "man getting haircut: Light Skin Tone", []string{"haircut_man_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f487\U0001f3ff\u200d\u2640\ufe0f", "woman getting haircut: Dark Skin Tone", []string{"haircut_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f487\U0001f3fc\u200d\u2642\ufe0f", "man getting haircut: Medium-Light Skin Tone", []string{"haircut_man_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f487\U0001f3fd\u200d\u2642\ufe0f", "man getting haircut: Medium Skin Tone", []string{"haircut_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fb\u200d\u2640\ufe0f", "woman getting haircut: Light Skin Tone", []string{"haircut_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fc\u200d\u2640\ufe0f", "woman getting haircut: Medium-Light Skin Tone", []string{"haircut_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fd\u200d\u2640\ufe0f", "woman getting haircut: Medium Skin Tone", []string{"haircut_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f487\U0001f3fe\u200d\u2640\ufe0f", "woman getting haircut: Medium-Dark Skin Tone", []string{"haircut_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\u270b\U0001f3ff", "raised hand: Dark Skin Tone", []string{"hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f487\U0001f3ff\u200d\u2640\ufe0f", "woman getting haircut: Dark Skin Tone", []string{"haircut_woman_Dark_Skin_Tone"}, "12.0", false}, {"\u270b\U0001f3fb", "raised hand: Light Skin Tone", []string{"hand_Light_Skin_Tone"}, "12.0", false}, {"\u270b\U0001f3fc", "raised hand: Medium-Light Skin Tone", []string{"hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\u270b\U0001f3fd", "raised hand: Medium Skin Tone", []string{"hand_Medium_Skin_Tone"}, "12.0", false}, {"\u270b\U0001f3fe", "raised hand: Medium-Dark Skin Tone", []string{"hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\u270b\U0001f3ff", "raised hand: Dark Skin Tone", []string{"hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf0\U0001f3fe", "hand with index finger and thumb crossed: Medium-Dark Skin Tone", []string{"hand_with_index_finger_and_thumb_crossed_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf0\U0001f3ff", "hand with index finger and thumb crossed: Dark Skin Tone", []string{"hand_with_index_finger_and_thumb_crossed_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf0\U0001f3fb", "hand with index finger and thumb crossed: Light Skin Tone", []string{"hand_with_index_finger_and_thumb_crossed_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf0\U0001f3fc", "hand with index finger and thumb crossed: Medium-Light Skin Tone", []string{"hand_with_index_finger_and_thumb_crossed_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf0\U0001f3fd", "hand with index finger and thumb crossed: Medium Skin Tone", []string{"hand_with_index_finger_and_thumb_crossed_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fd", "person playing handball: Medium Skin Tone", []string{"handball_person_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fe", "person playing handball: Medium-Dark Skin Tone", []string{"handball_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3ff", "person playing handball: Dark Skin Tone", []string{"handball_person_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fb", "person playing handball: Light Skin Tone", []string{"handball_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fc", "person playing handball: Medium-Light Skin Tone", []string{"handball_person_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f91d\U0001f3fb", "handshake: Light Skin Tone", []string{"handshake_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f91d\U0001f3fc", "handshake: Medium-Light Skin Tone", []string{"handshake_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f91d\U0001f3fd", "handshake: Medium Skin Tone", []string{"handshake_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f91d\U0001f3fe", "handshake: Medium-Dark Skin Tone", []string{"handshake_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f91d\U0001f3ff", "handshake: Dark Skin Tone", []string{"handshake_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\u2695\ufe0f", "health worker: Light Skin Tone", []string{"health_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\u2695\ufe0f", "health worker: Medium-Light Skin Tone", []string{"health_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\u2695\ufe0f", "health worker: Medium Skin Tone", []string{"health_worker_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\u2695\ufe0f", "health worker: Medium-Dark Skin Tone", []string{"health_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\u2695\ufe0f", "health worker: Dark Skin Tone", []string{"health_worker_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf6\U0001f3fe", "heart hands: Medium-Dark Skin Tone", []string{"heart_hands_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf6\U0001f3ff", "heart hands: Dark Skin Tone", []string{"heart_hands_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf6\U0001f3fb", "heart hands: Light Skin Tone", []string{"heart_hands_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf6\U0001f3fc", "heart hands: Medium-Light Skin Tone", []string{"heart_hands_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf6\U0001f3fd", "heart hands: Medium Skin Tone", []string{"heart_hands_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3c7\U0001f3fb", "horse racing: Light Skin Tone", []string{"horse_racing_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c7\U0001f3fc", "horse racing: Medium-Light Skin Tone", []string{"horse_racing_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c7\U0001f3fd", "horse racing: Medium Skin Tone", []string{"horse_racing_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3c7\U0001f3fe", "horse racing: Medium-Dark Skin Tone", []string{"horse_racing_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3c7\U0001f3ff", "horse racing: Dark Skin Tone", []string{"horse_racing_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf5\U0001f3fe", "index pointing at the viewer: Medium-Dark Skin Tone", []string{"index_pointing_at_the_viewer_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf5\U0001f3ff", "index pointing at the viewer: Dark Skin Tone", []string{"index_pointing_at_the_viewer_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf5\U0001f3fb", "index pointing at the viewer: Light Skin Tone", []string{"index_pointing_at_the_viewer_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf5\U0001f3fc", "index pointing at the viewer: Medium-Light Skin Tone", []string{"index_pointing_at_the_viewer_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf5\U0001f3fd", "index pointing at the viewer: Medium Skin Tone", []string{"index_pointing_at_the_viewer_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fd\u200d\u2696\ufe0f", "judge: Medium Skin Tone", []string{"judge_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\u2696\ufe0f", "judge: Medium-Dark Skin Tone", []string{"judge_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\u2696\ufe0f", "judge: Dark Skin Tone", []string{"judge_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\u2696\ufe0f", "judge: Light Skin Tone", []string{"judge_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\u2696\ufe0f", "judge: Medium-Light Skin Tone", []string{"judge_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fd\u200d\u2696\ufe0f", "judge: Medium Skin Tone", []string{"judge_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fe\u200d\u2696\ufe0f", "judge: Medium-Dark Skin Tone", []string{"judge_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f939\U0001f3fd", "person juggling: Medium Skin Tone", []string{"juggling_person_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f939\U0001f3fe", "person juggling: Medium-Dark Skin Tone", []string{"juggling_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3ff", "person juggling: Dark Skin Tone", []string{"juggling_person_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fb", "person juggling: Light Skin Tone", []string{"juggling_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fc", "person juggling: Medium-Light Skin Tone", []string{"juggling_person_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f939\U0001f3fd", "person juggling: Medium Skin Tone", []string{"juggling_person_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f939\U0001f3fe", "person juggling: Medium-Dark Skin Tone", []string{"juggling_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3fd\u200d\u2642\ufe0f", "man kneeling: Medium Skin Tone", []string{"kneeling_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3fe\u200d\u2642\ufe0f", "man kneeling: Medium-Dark Skin Tone", []string{"kneeling_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3ff\u200d\u2642\ufe0f", "man kneeling: Dark Skin Tone", []string{"kneeling_man_Dark_Skin_Tone"}, "12.0", false}, @@ -2205,51 +2336,56 @@ var GemojiData = Gemoji{ {"\U0001f9ce\U0001f3fd", "person kneeling: Medium Skin Tone", []string{"kneeling_person_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3fe", "person kneeling: Medium-Dark Skin Tone", []string{"kneeling_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3ff", "person kneeling: Dark Skin Tone", []string{"kneeling_person_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f", "woman kneeling: Light Skin Tone", []string{"kneeling_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3fc\u200d\u2640\ufe0f", "woman kneeling: Medium-Light Skin Tone", []string{"kneeling_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3fd\u200d\u2640\ufe0f", "woman kneeling: Medium Skin Tone", []string{"kneeling_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3fe\u200d\u2640\ufe0f", "woman kneeling: Medium-Dark Skin Tone", []string{"kneeling_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9ce\U0001f3ff\u200d\u2640\ufe0f", "woman kneeling: Dark Skin Tone", []string{"kneeling_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f", "woman kneeling: Light Skin Tone", []string{"kneeling_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf2\U0001f3fe", "leftwards hand: Medium-Dark Skin Tone", []string{"leftwards_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf2\U0001f3ff", "leftwards hand: Dark Skin Tone", []string{"leftwards_hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf2\U0001f3fb", "leftwards hand: Light Skin Tone", []string{"leftwards_hand_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf2\U0001f3fc", "leftwards hand: Medium-Light Skin Tone", []string{"leftwards_hand_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf2\U0001f3fd", "leftwards hand: Medium Skin Tone", []string{"leftwards_hand_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9b5\U0001f3fe", "leg: Medium-Dark Skin Tone", []string{"leg_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9b5\U0001f3ff", "leg: Dark Skin Tone", []string{"leg_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b5\U0001f3fb", "leg: Light Skin Tone", []string{"leg_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b5\U0001f3fc", "leg: Medium-Light Skin Tone", []string{"leg_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b5\U0001f3fd", "leg: Medium Skin Tone", []string{"leg_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9b5\U0001f3fe", "leg: Medium-Dark Skin Tone", []string{"leg_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9b5\U0001f3ff", "leg: Dark Skin Tone", []string{"leg_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fb", "person in lotus position: Light Skin Tone", []string{"lotus_position_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fc", "person in lotus position: Medium-Light Skin Tone", []string{"lotus_position_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fd", "person in lotus position: Medium Skin Tone", []string{"lotus_position_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fe", "person in lotus position: Medium-Dark Skin Tone", []string{"lotus_position_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3ff", "person in lotus position: Dark Skin Tone", []string{"lotus_position_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d8\U0001f3fd\u200d\u2642\ufe0f", "man in lotus position: Medium Skin Tone", []string{"lotus_position_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d8\U0001f3fe\u200d\u2642\ufe0f", "man in lotus position: Medium-Dark Skin Tone", []string{"lotus_position_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3ff\u200d\u2642\ufe0f", "man in lotus position: Dark Skin Tone", []string{"lotus_position_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fb\u200d\u2642\ufe0f", "man in lotus position: Light Skin Tone", []string{"lotus_position_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fc\u200d\u2642\ufe0f", "man in lotus position: Medium-Light Skin Tone", []string{"lotus_position_man_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d8\U0001f3fd\u200d\u2642\ufe0f", "man in lotus position: Medium Skin Tone", []string{"lotus_position_man_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d8\U0001f3fe\u200d\u2642\ufe0f", "man in lotus position: Medium-Dark Skin Tone", []string{"lotus_position_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d8\U0001f3fe\u200d\u2640\ufe0f", "woman in lotus position: Medium-Dark Skin Tone", []string{"lotus_position_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d8\U0001f3ff\u200d\u2640\ufe0f", "woman in lotus position: Dark Skin Tone", []string{"lotus_position_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fb\u200d\u2640\ufe0f", "woman in lotus position: Light Skin Tone", []string{"lotus_position_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fc\u200d\u2640\ufe0f", "woman in lotus position: Medium-Light Skin Tone", []string{"lotus_position_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d8\U0001f3fd\u200d\u2640\ufe0f", "woman in lotus position: Medium Skin Tone", []string{"lotus_position_woman_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d8\U0001f3fe\u200d\u2640\ufe0f", "woman in lotus position: Medium-Dark Skin Tone", []string{"lotus_position_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d8\U0001f3ff\u200d\u2640\ufe0f", "woman in lotus position: Dark Skin Tone", []string{"lotus_position_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f91f\U0001f3ff", "love-you gesture: Dark Skin Tone", []string{"love_you_gesture_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f91f\U0001f3fb", "love-you gesture: Light Skin Tone", []string{"love_you_gesture_Light_Skin_Tone"}, "12.0", false}, {"\U0001f91f\U0001f3fc", "love-you gesture: Medium-Light Skin Tone", []string{"love_you_gesture_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f91f\U0001f3fd", "love-you gesture: Medium Skin Tone", []string{"love_you_gesture_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f91f\U0001f3fe", "love-you gesture: Medium-Dark Skin Tone", []string{"love_you_gesture_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f91f\U0001f3ff", "love-you gesture: Dark Skin Tone", []string{"love_you_gesture_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fb", "mage: Light Skin Tone", []string{"mage_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fc", "mage: Medium-Light Skin Tone", []string{"mage_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fd", "mage: Medium Skin Tone", []string{"mage_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fe", "mage: Medium-Dark Skin Tone", []string{"mage_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3ff", "mage: Dark Skin Tone", []string{"mage_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d9\U0001f3fe\u200d\u2642\ufe0f", "man mage: Medium-Dark Skin Tone", []string{"mage_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d9\U0001f3ff\u200d\u2642\ufe0f", "man mage: Dark Skin Tone", []string{"mage_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fb\u200d\u2642\ufe0f", "man mage: Light Skin Tone", []string{"mage_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fc\u200d\u2642\ufe0f", "man mage: Medium-Light Skin Tone", []string{"mage_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fd\u200d\u2642\ufe0f", "man mage: Medium Skin Tone", []string{"mage_man_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d9\U0001f3fe\u200d\u2642\ufe0f", "man mage: Medium-Dark Skin Tone", []string{"mage_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d9\U0001f3ff\u200d\u2642\ufe0f", "man mage: Dark Skin Tone", []string{"mage_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d9\U0001f3fb\u200d\u2640\ufe0f", "woman mage: Light Skin Tone", []string{"mage_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d9\U0001f3fc\u200d\u2640\ufe0f", "woman mage: Medium-Light Skin Tone", []string{"mage_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fd\u200d\u2640\ufe0f", "woman mage: Medium Skin Tone", []string{"mage_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3fe\u200d\u2640\ufe0f", "woman mage: Medium-Dark Skin Tone", []string{"mage_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d9\U0001f3ff\u200d\u2640\ufe0f", "woman mage: Dark Skin Tone", []string{"mage_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d9\U0001f3fb\u200d\u2640\ufe0f", "woman mage: Light Skin Tone", []string{"mage_woman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d9\U0001f3fc\u200d\u2640\ufe0f", "woman mage: Medium-Light Skin Tone", []string{"mage_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fb\ufe0f\u200d\u2642\ufe0f", "man detective: Light Skin Tone", []string{"male_detective_Light_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fc\ufe0f\u200d\u2642\ufe0f", "man detective: Medium-Light Skin Tone", []string{"male_detective_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f575\U0001f3fd\ufe0f\u200d\u2642\ufe0f", "man detective: Medium Skin Tone", []string{"male_detective_Medium_Skin_Tone"}, "12.0", false}, @@ -2260,16 +2396,21 @@ var GemojiData = Gemoji{ {"\U0001f468\U0001f3fd", "man: Medium Skin Tone", []string{"man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe", "man: Medium-Dark Skin Tone", []string{"man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff", "man: Dark Skin Tone", []string{"man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f3a8", "man artist: Light Skin Tone", []string{"man_artist_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f3a8", "man artist: Medium-Light Skin Tone", []string{"man_artist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f3a8", "man artist: Medium Skin Tone", []string{"man_artist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f3a8", "man artist: Medium-Dark Skin Tone", []string{"man_artist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f3a8", "man artist: Dark Skin Tone", []string{"man_artist_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f3a8", "man artist: Light Skin Tone", []string{"man_artist_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f3a8", "man artist: Medium-Light Skin Tone", []string{"man_artist_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f680", "man astronaut: Light Skin Tone", []string{"man_astronaut_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f680", "man astronaut: Medium-Light Skin Tone", []string{"man_astronaut_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f680", "man astronaut: Medium Skin Tone", []string{"man_astronaut_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f680", "man astronaut: Medium-Dark Skin Tone", []string{"man_astronaut_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f680", "man astronaut: Dark Skin Tone", []string{"man_astronaut_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f680", "man astronaut: Light Skin Tone", []string{"man_astronaut_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3ff\u200d\u2642\ufe0f", "man: beard: Dark Skin Tone", []string{"man_beard_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fb\u200d\u2642\ufe0f", "man: beard: Light Skin Tone", []string{"man_beard_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fc\u200d\u2642\ufe0f", "man: beard: Medium-Light Skin Tone", []string{"man_beard_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fd\u200d\u2642\ufe0f", "man: beard: Medium Skin Tone", []string{"man_beard_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fe\u200d\u2642\ufe0f", "man: beard: Medium-Dark Skin Tone", []string{"man_beard_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fb\u200d\u2642\ufe0f", "man cartwheeling: Light Skin Tone", []string{"man_cartwheeling_Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fc\u200d\u2642\ufe0f", "man cartwheeling: Medium-Light Skin Tone", []string{"man_cartwheeling_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fd\u200d\u2642\ufe0f", "man cartwheeling: Medium Skin Tone", []string{"man_cartwheeling_Medium_Skin_Tone"}, "12.0", false}, @@ -2280,176 +2421,191 @@ var GemojiData = Gemoji{ {"\U0001f468\U0001f3fc\u200d\U0001f373", "man cook: Medium-Light Skin Tone", []string{"man_cook_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f373", "man cook: Medium Skin Tone", []string{"man_cook_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f373", "man cook: Medium-Dark Skin Tone", []string{"man_cook_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f57a\U0001f3fe", "man dancing: Medium-Dark Skin Tone", []string{"man_dancing_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f57a\U0001f3ff", "man dancing: Dark Skin Tone", []string{"man_dancing_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f57a\U0001f3fb", "man dancing: Light Skin Tone", []string{"man_dancing_Light_Skin_Tone"}, "12.0", false}, {"\U0001f57a\U0001f3fc", "man dancing: Medium-Light Skin Tone", []string{"man_dancing_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f57a\U0001f3fd", "man dancing: Medium Skin Tone", []string{"man_dancing_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f57a\U0001f3fe", "man dancing: Medium-Dark Skin Tone", []string{"man_dancing_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f57a\U0001f3ff", "man dancing: Dark Skin Tone", []string{"man_dancing_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f926\U0001f3fe\u200d\u2642\ufe0f", "man facepalming: Medium-Dark Skin Tone", []string{"man_facepalming_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3ff\u200d\u2642\ufe0f", "man facepalming: Dark Skin Tone", []string{"man_facepalming_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fb\u200d\u2642\ufe0f", "man facepalming: Light Skin Tone", []string{"man_facepalming_Light_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fc\u200d\u2642\ufe0f", "man facepalming: Medium-Light Skin Tone", []string{"man_facepalming_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fd\u200d\u2642\ufe0f", "man facepalming: Medium Skin Tone", []string{"man_facepalming_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f926\U0001f3fe\u200d\u2642\ufe0f", "man facepalming: Medium-Dark Skin Tone", []string{"man_facepalming_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f3ed", "man factory worker: Medium-Dark Skin Tone", []string{"man_factory_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3ff\u200d\U0001f3ed", "man factory worker: Dark Skin Tone", []string{"man_factory_worker_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f3ed", "man factory worker: Light Skin Tone", []string{"man_factory_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f3ed", "man factory worker: Medium-Light Skin Tone", []string{"man_factory_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f3ed", "man factory worker: Medium Skin Tone", []string{"man_factory_worker_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fe\u200d\U0001f3ed", "man factory worker: Medium-Dark Skin Tone", []string{"man_factory_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3ff\u200d\U0001f3ed", "man factory worker: Dark Skin Tone", []string{"man_factory_worker_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f33e", "man farmer: Light Skin Tone", []string{"man_farmer_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f33e", "man farmer: Medium-Light Skin Tone", []string{"man_farmer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f33e", "man farmer: Medium Skin Tone", []string{"man_farmer_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f33e", "man farmer: Medium-Dark Skin Tone", []string{"man_farmer_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f33e", "man farmer: Dark Skin Tone", []string{"man_farmer_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f692", "man firefighter: Light Skin Tone", []string{"man_firefighter_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f692", "man firefighter: Medium-Light Skin Tone", []string{"man_firefighter_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f33e", "man farmer: Light Skin Tone", []string{"man_farmer_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fd\u200d\U0001f37c", "man feeding baby: Medium Skin Tone", []string{"man_feeding_baby_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f37c", "man feeding baby: Medium-Dark Skin Tone", []string{"man_feeding_baby_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3ff\u200d\U0001f37c", "man feeding baby: Dark Skin Tone", []string{"man_feeding_baby_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f37c", "man feeding baby: Light Skin Tone", []string{"man_feeding_baby_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f37c", "man feeding baby: Medium-Light Skin Tone", []string{"man_feeding_baby_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f692", "man firefighter: Medium Skin Tone", []string{"man_firefighter_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f692", "man firefighter: Medium-Dark Skin Tone", []string{"man_firefighter_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f692", "man firefighter: Dark Skin Tone", []string{"man_firefighter_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fe\u200d\u2695\ufe0f", "man health worker: Medium-Dark Skin Tone", []string{"man_health_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3ff\u200d\u2695\ufe0f", "man health worker: Dark Skin Tone", []string{"man_health_worker_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f692", "man firefighter: Light Skin Tone", []string{"man_firefighter_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f692", "man firefighter: Medium-Light Skin Tone", []string{"man_firefighter_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\u2695\ufe0f", "man health worker: Light Skin Tone", []string{"man_health_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\u2695\ufe0f", "man health worker: Medium-Light Skin Tone", []string{"man_health_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\u2695\ufe0f", "man health worker: Medium Skin Tone", []string{"man_health_worker_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\u2695\ufe0f", "man health worker: Medium-Dark Skin Tone", []string{"man_health_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3ff\u200d\u2695\ufe0f", "man health worker: Dark Skin Tone", []string{"man_health_worker_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f9bd", "man in manual wheelchair: Light Skin Tone", []string{"man_in_manual_wheelchair_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f9bd", "man in manual wheelchair: Medium-Light Skin Tone", []string{"man_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f9bd", "man in manual wheelchair: Medium Skin Tone", []string{"man_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f9bd", "man in manual wheelchair: Medium-Dark Skin Tone", []string{"man_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f9bd", "man in manual wheelchair: Dark Skin Tone", []string{"man_in_manual_wheelchair_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f9bd", "man in manual wheelchair: Light Skin Tone", []string{"man_in_manual_wheelchair_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f9bd", "man in manual wheelchair: Medium-Light Skin Tone", []string{"man_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f9bc", "man in motorized wheelchair: Light Skin Tone", []string{"man_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f9bc", "man in motorized wheelchair: Medium-Light Skin Tone", []string{"man_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f9bc", "man in motorized wheelchair: Medium Skin Tone", []string{"man_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f9bc", "man in motorized wheelchair: Medium-Dark Skin Tone", []string{"man_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f9bc", "man in motorized wheelchair: Dark Skin Tone", []string{"man_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f9bc", "man in motorized wheelchair: Light Skin Tone", []string{"man_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f9bc", "man in motorized wheelchair: Medium-Light Skin Tone", []string{"man_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fb\u200d\u2642\ufe0f", "man in tuxedo: Light Skin Tone", []string{"man_in_tuxedo_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fc\u200d\u2642\ufe0f", "man in tuxedo: Medium-Light Skin Tone", []string{"man_in_tuxedo_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fd\u200d\u2642\ufe0f", "man in tuxedo: Medium Skin Tone", []string{"man_in_tuxedo_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fe\u200d\u2642\ufe0f", "man in tuxedo: Medium-Dark Skin Tone", []string{"man_in_tuxedo_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3ff\u200d\u2642\ufe0f", "man in tuxedo: Dark Skin Tone", []string{"man_in_tuxedo_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\u2696\ufe0f", "man judge: Light Skin Tone", []string{"man_judge_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\u2696\ufe0f", "man judge: Medium-Light Skin Tone", []string{"man_judge_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\u2696\ufe0f", "man judge: Medium Skin Tone", []string{"man_judge_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\u2696\ufe0f", "man judge: Medium-Dark Skin Tone", []string{"man_judge_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\u2696\ufe0f", "man judge: Dark Skin Tone", []string{"man_judge_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f939\U0001f3fc\u200d\u2642\ufe0f", "man juggling: Medium-Light Skin Tone", []string{"man_juggling_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fd\u200d\u2642\ufe0f", "man juggling: Medium Skin Tone", []string{"man_juggling_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fe\u200d\u2642\ufe0f", "man juggling: Medium-Dark Skin Tone", []string{"man_juggling_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3ff\u200d\u2642\ufe0f", "man juggling: Dark Skin Tone", []string{"man_juggling_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fb\u200d\u2642\ufe0f", "man juggling: Light Skin Tone", []string{"man_juggling_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f939\U0001f3fc\u200d\u2642\ufe0f", "man juggling: Medium-Light Skin Tone", []string{"man_juggling_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f527", "man mechanic: Light Skin Tone", []string{"man_mechanic_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f527", "man mechanic: Medium-Light Skin Tone", []string{"man_mechanic_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f527", "man mechanic: Medium Skin Tone", []string{"man_mechanic_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f527", "man mechanic: Medium-Dark Skin Tone", []string{"man_mechanic_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f527", "man mechanic: Dark Skin Tone", []string{"man_mechanic_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f527", "man mechanic: Light Skin Tone", []string{"man_mechanic_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f527", "man mechanic: Medium-Light Skin Tone", []string{"man_mechanic_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f4bc", "man office worker: Light Skin Tone", []string{"man_office_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f4bc", "man office worker: Medium-Light Skin Tone", []string{"man_office_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f4bc", "man office worker: Medium Skin Tone", []string{"man_office_worker_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f4bc", "man office worker: Medium-Dark Skin Tone", []string{"man_office_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f4bc", "man office worker: Dark Skin Tone", []string{"man_office_worker_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f4bc", "man office worker: Light Skin Tone", []string{"man_office_worker_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\u2708\ufe0f", "man pilot: Light Skin Tone", []string{"man_pilot_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\u2708\ufe0f", "man pilot: Medium-Light Skin Tone", []string{"man_pilot_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\u2708\ufe0f", "man pilot: Medium Skin Tone", []string{"man_pilot_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\u2708\ufe0f", "man pilot: Medium-Dark Skin Tone", []string{"man_pilot_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\u2708\ufe0f", "man pilot: Dark Skin Tone", []string{"man_pilot_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\u2708\ufe0f", "man pilot: Light Skin Tone", []string{"man_pilot_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\u2708\ufe0f", "man pilot: Medium-Light Skin Tone", []string{"man_pilot_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fb\u200d\u2642\ufe0f", "man playing handball: Light Skin Tone", []string{"man_playing_handball_Light_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fc\u200d\u2642\ufe0f", "man playing handball: Medium-Light Skin Tone", []string{"man_playing_handball_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fd\u200d\u2642\ufe0f", "man playing handball: Medium Skin Tone", []string{"man_playing_handball_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fe\u200d\u2642\ufe0f", "man playing handball: Medium-Dark Skin Tone", []string{"man_playing_handball_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3ff\u200d\u2642\ufe0f", "man playing handball: Dark Skin Tone", []string{"man_playing_handball_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f93d\U0001f3fb\u200d\u2642\ufe0f", "man playing water polo: Light Skin Tone", []string{"man_playing_water_polo_Light_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fc\u200d\u2642\ufe0f", "man playing water polo: Medium-Light Skin Tone", []string{"man_playing_water_polo_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fd\u200d\u2642\ufe0f", "man playing water polo: Medium Skin Tone", []string{"man_playing_water_polo_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fe\u200d\u2642\ufe0f", "man playing water polo: Medium-Dark Skin Tone", []string{"man_playing_water_polo_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3ff\u200d\u2642\ufe0f", "man playing water polo: Dark Skin Tone", []string{"man_playing_water_polo_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f52c", "man scientist: Light Skin Tone", []string{"man_scientist_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f52c", "man scientist: Medium-Light Skin Tone", []string{"man_scientist_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f93d\U0001f3fb\u200d\u2642\ufe0f", "man playing water polo: Light Skin Tone", []string{"man_playing_water_polo_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f52c", "man scientist: Medium Skin Tone", []string{"man_scientist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f52c", "man scientist: Medium-Dark Skin Tone", []string{"man_scientist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f52c", "man scientist: Dark Skin Tone", []string{"man_scientist_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f937\U0001f3fe\u200d\u2642\ufe0f", "man shrugging: Medium-Dark Skin Tone", []string{"man_shrugging_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f937\U0001f3ff\u200d\u2642\ufe0f", "man shrugging: Dark Skin Tone", []string{"man_shrugging_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f52c", "man scientist: Light Skin Tone", []string{"man_scientist_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f52c", "man scientist: Medium-Light Skin Tone", []string{"man_scientist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fb\u200d\u2642\ufe0f", "man shrugging: Light Skin Tone", []string{"man_shrugging_Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fc\u200d\u2642\ufe0f", "man shrugging: Medium-Light Skin Tone", []string{"man_shrugging_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fd\u200d\u2642\ufe0f", "man shrugging: Medium Skin Tone", []string{"man_shrugging_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f937\U0001f3fe\u200d\u2642\ufe0f", "man shrugging: Medium-Dark Skin Tone", []string{"man_shrugging_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f937\U0001f3ff\u200d\u2642\ufe0f", "man shrugging: Dark Skin Tone", []string{"man_shrugging_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f3a4", "man singer: Medium-Dark Skin Tone", []string{"man_singer_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3ff\u200d\U0001f3a4", "man singer: Dark Skin Tone", []string{"man_singer_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f3a4", "man singer: Light Skin Tone", []string{"man_singer_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f3a4", "man singer: Medium-Light Skin Tone", []string{"man_singer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f3a4", "man singer: Medium Skin Tone", []string{"man_singer_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fe\u200d\U0001f3a4", "man singer: Medium-Dark Skin Tone", []string{"man_singer_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3ff\u200d\U0001f3a4", "man singer: Dark Skin Tone", []string{"man_singer_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3ff\u200d\U0001f393", "man student: Dark Skin Tone", []string{"man_student_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f393", "man student: Light Skin Tone", []string{"man_student_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f393", "man student: Medium-Light Skin Tone", []string{"man_student_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f393", "man student: Medium Skin Tone", []string{"man_student_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f393", "man student: Medium-Dark Skin Tone", []string{"man_student_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3ff\u200d\U0001f393", "man student: Dark Skin Tone", []string{"man_student_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f3eb", "man teacher: Medium-Dark Skin Tone", []string{"man_teacher_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f3eb", "man teacher: Dark Skin Tone", []string{"man_teacher_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f3eb", "man teacher: Light Skin Tone", []string{"man_teacher_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f3eb", "man teacher: Medium-Light Skin Tone", []string{"man_teacher_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f3eb", "man teacher: Medium Skin Tone", []string{"man_teacher_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fe\u200d\U0001f3eb", "man teacher: Medium-Dark Skin Tone", []string{"man_teacher_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f4bb", "man technologist: Light Skin Tone", []string{"man_technologist_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f4bb", "man technologist: Medium-Light Skin Tone", []string{"man_technologist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f4bb", "man technologist: Medium Skin Tone", []string{"man_technologist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f4bb", "man technologist: Medium-Dark Skin Tone", []string{"man_technologist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f4bb", "man technologist: Dark Skin Tone", []string{"man_technologist_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f472\U0001f3fe", "person with skullcap: Medium-Dark Skin Tone", []string{"man_with_gua_pi_mao_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f4bb", "man technologist: Light Skin Tone", []string{"man_technologist_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f4bb", "man technologist: Medium-Light Skin Tone", []string{"man_technologist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f472\U0001f3ff", "person with skullcap: Dark Skin Tone", []string{"man_with_gua_pi_mao_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f472\U0001f3fb", "person with skullcap: Light Skin Tone", []string{"man_with_gua_pi_mao_Light_Skin_Tone"}, "12.0", false}, {"\U0001f472\U0001f3fc", "person with skullcap: Medium-Light Skin Tone", []string{"man_with_gua_pi_mao_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f472\U0001f3fd", "person with skullcap: Medium Skin Tone", []string{"man_with_gua_pi_mao_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f472\U0001f3fe", "person with skullcap: Medium-Dark Skin Tone", []string{"man_with_gua_pi_mao_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f9af", "man with white cane: Light Skin Tone", []string{"man_with_probing_cane_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f9af", "man with white cane: Medium-Light Skin Tone", []string{"man_with_probing_cane_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f9af", "man with white cane: Medium Skin Tone", []string{"man_with_probing_cane_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f9af", "man with white cane: Medium-Dark Skin Tone", []string{"man_with_probing_cane_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f9af", "man with white cane: Dark Skin Tone", []string{"man_with_probing_cane_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f473\U0001f3fc\u200d\u2642\ufe0f", "man wearing turban: Medium-Light Skin Tone", []string{"man_with_turban_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f473\U0001f3fd\u200d\u2642\ufe0f", "man wearing turban: Medium Skin Tone", []string{"man_with_turban_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fe\u200d\u2642\ufe0f", "man wearing turban: Medium-Dark Skin Tone", []string{"man_with_turban_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3ff\u200d\u2642\ufe0f", "man wearing turban: Dark Skin Tone", []string{"man_with_turban_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fb\u200d\u2642\ufe0f", "man wearing turban: Light Skin Tone", []string{"man_with_turban_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f473\U0001f3fc\u200d\u2642\ufe0f", "man wearing turban: Medium-Light Skin Tone", []string{"man_with_turban_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f473\U0001f3fd\u200d\u2642\ufe0f", "man wearing turban: Medium Skin Tone", []string{"man_with_turban_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3ff\u200d\u2642\ufe0f", "man with veil: Dark Skin Tone", []string{"man_with_veil_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fb\u200d\u2642\ufe0f", "man with veil: Light Skin Tone", []string{"man_with_veil_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fc\u200d\u2642\ufe0f", "man with veil: Medium-Light Skin Tone", []string{"man_with_veil_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fd\u200d\u2642\ufe0f", "man with veil: Medium Skin Tone", []string{"man_with_veil_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fe\u200d\u2642\ufe0f", "man with veil: Medium-Dark Skin Tone", []string{"man_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fb", "person getting massage: Light Skin Tone", []string{"massage_Light_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fc", "person getting massage: Medium-Light Skin Tone", []string{"massage_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fd", "person getting massage: Medium Skin Tone", []string{"massage_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fe", "person getting massage: Medium-Dark Skin Tone", []string{"massage_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3ff", "person getting massage: Dark Skin Tone", []string{"massage_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f486\U0001f3ff\u200d\u2642\ufe0f", "man getting massage: Dark Skin Tone", []string{"massage_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fb\u200d\u2642\ufe0f", "man getting massage: Light Skin Tone", []string{"massage_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fc\u200d\u2642\ufe0f", "man getting massage: Medium-Light Skin Tone", []string{"massage_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fd\u200d\u2642\ufe0f", "man getting massage: Medium Skin Tone", []string{"massage_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fe\u200d\u2642\ufe0f", "man getting massage: Medium-Dark Skin Tone", []string{"massage_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f486\U0001f3ff\u200d\u2642\ufe0f", "man getting massage: Dark Skin Tone", []string{"massage_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f486\U0001f3fb\u200d\u2640\ufe0f", "woman getting massage: Light Skin Tone", []string{"massage_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fc\u200d\u2640\ufe0f", "woman getting massage: Medium-Light Skin Tone", []string{"massage_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fd\u200d\u2640\ufe0f", "woman getting massage: Medium Skin Tone", []string{"massage_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3fe\u200d\u2640\ufe0f", "woman getting massage: Medium-Dark Skin Tone", []string{"massage_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f486\U0001f3ff\u200d\u2640\ufe0f", "woman getting massage: Dark Skin Tone", []string{"massage_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f486\U0001f3fb\u200d\u2640\ufe0f", "woman getting massage: Light Skin Tone", []string{"massage_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f527", "mechanic: Medium-Dark Skin Tone", []string{"mechanic_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f527", "mechanic: Dark Skin Tone", []string{"mechanic_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f527", "mechanic: Light Skin Tone", []string{"mechanic_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f527", "mechanic: Medium-Light Skin Tone", []string{"mechanic_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f527", "mechanic: Medium Skin Tone", []string{"mechanic_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9dc\U0001f3fe\u200d\u2640\ufe0f", "mermaid: Medium-Dark Skin Tone", []string{"mermaid_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9dc\U0001f3ff\u200d\u2640\ufe0f", "mermaid: Dark Skin Tone", []string{"mermaid_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fb\u200d\u2640\ufe0f", "mermaid: Light Skin Tone", []string{"mermaid_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fc\u200d\u2640\ufe0f", "mermaid: Medium-Light Skin Tone", []string{"mermaid_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fd\u200d\u2640\ufe0f", "mermaid: Medium Skin Tone", []string{"mermaid_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9dc\U0001f3fe\u200d\u2640\ufe0f", "mermaid: Medium-Dark Skin Tone", []string{"mermaid_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9dc\U0001f3ff\u200d\u2640\ufe0f", "mermaid: Dark Skin Tone", []string{"mermaid_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9dc\U0001f3fc\u200d\u2642\ufe0f", "merman: Medium-Light Skin Tone", []string{"merman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9dc\U0001f3fd\u200d\u2642\ufe0f", "merman: Medium Skin Tone", []string{"merman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fe\u200d\u2642\ufe0f", "merman: Medium-Dark Skin Tone", []string{"merman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3ff\u200d\u2642\ufe0f", "merman: Dark Skin Tone", []string{"merman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fb\u200d\u2642\ufe0f", "merman: Light Skin Tone", []string{"merman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9dc\U0001f3fc\u200d\u2642\ufe0f", "merman: Medium-Light Skin Tone", []string{"merman_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9dc\U0001f3fd\u200d\u2642\ufe0f", "merman: Medium Skin Tone", []string{"merman_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9dc\U0001f3fb", "merperson: Light Skin Tone", []string{"merperson_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fc", "merperson: Medium-Light Skin Tone", []string{"merperson_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fd", "merperson: Medium Skin Tone", []string{"merperson_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3fe", "merperson: Medium-Dark Skin Tone", []string{"merperson_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9dc\U0001f3ff", "merperson: Dark Skin Tone", []string{"merperson_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9dc\U0001f3fb", "merperson: Light Skin Tone", []string{"merperson_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f918\U0001f3fb", "sign of the horns: Light Skin Tone", []string{"metal_Light_Skin_Tone"}, "12.0", false}, {"\U0001f918\U0001f3fc", "sign of the horns: Medium-Light Skin Tone", []string{"metal_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f918\U0001f3fd", "sign of the horns: Medium Skin Tone", []string{"metal_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f918\U0001f3fe", "sign of the horns: Medium-Dark Skin Tone", []string{"metal_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f918\U0001f3ff", "sign of the horns: Dark Skin Tone", []string{"metal_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f918\U0001f3fb", "sign of the horns: Light Skin Tone", []string{"metal_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f595\U0001f3ff", "middle finger: Dark Skin Tone", []string{"middle_finger_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f595\U0001f3fb", "middle finger: Light Skin Tone", []string{"middle_finger_Light_Skin_Tone"}, "12.0", false}, {"\U0001f595\U0001f3fc", "middle finger: Medium-Light Skin Tone", []string{"middle_finger_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f595\U0001f3fd", "middle finger: Medium Skin Tone", []string{"middle_finger_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f595\U0001f3fe", "middle finger: Medium-Dark Skin Tone", []string{"middle_finger_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f595\U0001f3ff", "middle finger: Dark Skin Tone", []string{"middle_finger_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f595\U0001f3fb", "middle finger: Light Skin Tone", []string{"middle_finger_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6b5\U0001f3fe", "person mountain biking: Medium-Dark Skin Tone", []string{"mountain_bicyclist_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6b5\U0001f3ff", "person mountain biking: Dark Skin Tone", []string{"mountain_bicyclist_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b5\U0001f3fb", "person mountain biking: Light Skin Tone", []string{"mountain_bicyclist_Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b5\U0001f3fc", "person mountain biking: Medium-Light Skin Tone", []string{"mountain_bicyclist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b5\U0001f3fd", "person mountain biking: Medium Skin Tone", []string{"mountain_bicyclist_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f6b5\U0001f3fe", "person mountain biking: Medium-Dark Skin Tone", []string{"mountain_bicyclist_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b5\U0001f3ff", "person mountain biking: Dark Skin Tone", []string{"mountain_bicyclist_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b5\U0001f3fb\u200d\u2642\ufe0f", "man mountain biking: Light Skin Tone", []string{"mountain_biking_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b5\U0001f3fc\u200d\u2642\ufe0f", "man mountain biking: Medium-Light Skin Tone", []string{"mountain_biking_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b5\U0001f3fd\u200d\u2642\ufe0f", "man mountain biking: Medium Skin Tone", []string{"mountain_biking_man_Medium_Skin_Tone"}, "12.0", false}, @@ -2465,26 +2621,36 @@ var GemojiData = Gemoji{ {"\U0001f936\U0001f3fd", "Mrs. Claus: Medium Skin Tone", []string{"mrs_claus_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f936\U0001f3fe", "Mrs. Claus: Medium-Dark Skin Tone", []string{"mrs_claus_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f936\U0001f3ff", "Mrs. Claus: Dark Skin Tone", []string{"mrs_claus_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f4aa\U0001f3fe", "flexed biceps: Medium-Dark Skin Tone", []string{"muscle_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f4aa\U0001f3ff", "flexed biceps: Dark Skin Tone", []string{"muscle_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f4aa\U0001f3fb", "flexed biceps: Light Skin Tone", []string{"muscle_Light_Skin_Tone"}, "12.0", false}, {"\U0001f4aa\U0001f3fc", "flexed biceps: Medium-Light Skin Tone", []string{"muscle_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f4aa\U0001f3fd", "flexed biceps: Medium Skin Tone", []string{"muscle_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f4aa\U0001f3fe", "flexed biceps: Medium-Dark Skin Tone", []string{"muscle_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f4aa\U0001f3ff", "flexed biceps: Dark Skin Tone", []string{"muscle_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fd\u200d\U0001f384", "mx claus: Medium Skin Tone", []string{"mx_claus_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f384", "mx claus: Medium-Dark Skin Tone", []string{"mx_claus_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f384", "mx claus: Dark Skin Tone", []string{"mx_claus_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f384", "mx claus: Light Skin Tone", []string{"mx_claus_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f384", "mx claus: Medium-Light Skin Tone", []string{"mx_claus_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f485\U0001f3fe", "nail polish: Medium-Dark Skin Tone", []string{"nail_care_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f485\U0001f3ff", "nail polish: Dark Skin Tone", []string{"nail_care_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f485\U0001f3fb", "nail polish: Light Skin Tone", []string{"nail_care_Light_Skin_Tone"}, "12.0", false}, {"\U0001f485\U0001f3fc", "nail polish: Medium-Light Skin Tone", []string{"nail_care_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f485\U0001f3fd", "nail polish: Medium Skin Tone", []string{"nail_care_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f485\U0001f3fe", "nail polish: Medium-Dark Skin Tone", []string{"nail_care_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f485\U0001f3ff", "nail polish: Dark Skin Tone", []string{"nail_care_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f977\U0001f3fb", "ninja: Light Skin Tone", []string{"ninja_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f977\U0001f3fc", "ninja: Medium-Light Skin Tone", []string{"ninja_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f977\U0001f3fd", "ninja: Medium Skin Tone", []string{"ninja_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f977\U0001f3fe", "ninja: Medium-Dark Skin Tone", []string{"ninja_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f977\U0001f3ff", "ninja: Dark Skin Tone", []string{"ninja_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fb", "person gesturing NO: Light Skin Tone", []string{"no_good_Light_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fc", "person gesturing NO: Medium-Light Skin Tone", []string{"no_good_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fd", "person gesturing NO: Medium Skin Tone", []string{"no_good_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fe", "person gesturing NO: Medium-Dark Skin Tone", []string{"no_good_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3ff", "person gesturing NO: Dark Skin Tone", []string{"no_good_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f645\U0001f3ff\u200d\u2642\ufe0f", "man gesturing NO: Dark Skin Tone", []string{"no_good_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f645\U0001f3fb\u200d\u2642\ufe0f", "man gesturing NO: Light Skin Tone", []string{"no_good_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fc\u200d\u2642\ufe0f", "man gesturing NO: Medium-Light Skin Tone", []string{"no_good_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fd\u200d\u2642\ufe0f", "man gesturing NO: Medium Skin Tone", []string{"no_good_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fe\u200d\u2642\ufe0f", "man gesturing NO: Medium-Dark Skin Tone", []string{"no_good_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f645\U0001f3ff\u200d\u2642\ufe0f", "man gesturing NO: Dark Skin Tone", []string{"no_good_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f645\U0001f3fb\u200d\u2642\ufe0f", "man gesturing NO: Light Skin Tone", []string{"no_good_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fb\u200d\u2640\ufe0f", "woman gesturing NO: Light Skin Tone", []string{"no_good_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fc\u200d\u2640\ufe0f", "woman gesturing NO: Medium-Light Skin Tone", []string{"no_good_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f645\U0001f3fd\u200d\u2640\ufe0f", "woman gesturing NO: Medium Skin Tone", []string{"no_good_woman_Medium_Skin_Tone"}, "12.0", false}, @@ -2495,251 +2661,286 @@ var GemojiData = Gemoji{ {"\U0001f443\U0001f3fd", "nose: Medium Skin Tone", []string{"nose_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f443\U0001f3fe", "nose: Medium-Dark Skin Tone", []string{"nose_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f443\U0001f3ff", "nose: Dark Skin Tone", []string{"nose_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f4bc", "office worker: Light Skin Tone", []string{"office_worker_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f4bc", "office worker: Medium-Light Skin Tone", []string{"office_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f4bc", "office worker: Medium Skin Tone", []string{"office_worker_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f4bc", "office worker: Medium-Dark Skin Tone", []string{"office_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f4bc", "office worker: Dark Skin Tone", []string{"office_worker_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f4bc", "office worker: Light Skin Tone", []string{"office_worker_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fc\u200d\U0001f4bc", "office worker: Medium-Light Skin Tone", []string{"office_worker_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f44c\U0001f3fe", "OK hand: Medium-Dark Skin Tone", []string{"ok_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f44c\U0001f3ff", "OK hand: Dark Skin Tone", []string{"ok_hand_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f44c\U0001f3fb", "OK hand: Light Skin Tone", []string{"ok_hand_Light_Skin_Tone"}, "12.0", false}, {"\U0001f44c\U0001f3fc", "OK hand: Medium-Light Skin Tone", []string{"ok_hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f44c\U0001f3fd", "OK hand: Medium Skin Tone", []string{"ok_hand_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f44c\U0001f3fe", "OK hand: Medium-Dark Skin Tone", []string{"ok_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f44c\U0001f3ff", "OK hand: Dark Skin Tone", []string{"ok_hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f646\U0001f3ff\u200d\u2642\ufe0f", "man gesturing OK: Dark Skin Tone", []string{"ok_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fb\u200d\u2642\ufe0f", "man gesturing OK: Light Skin Tone", []string{"ok_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fc\u200d\u2642\ufe0f", "man gesturing OK: Medium-Light Skin Tone", []string{"ok_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fd\u200d\u2642\ufe0f", "man gesturing OK: Medium Skin Tone", []string{"ok_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fe\u200d\u2642\ufe0f", "man gesturing OK: Medium-Dark Skin Tone", []string{"ok_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f646\U0001f3ff\u200d\u2642\ufe0f", "man gesturing OK: Dark Skin Tone", []string{"ok_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f646\U0001f3ff", "person gesturing OK: Dark Skin Tone", []string{"ok_person_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fb", "person gesturing OK: Light Skin Tone", []string{"ok_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fc", "person gesturing OK: Medium-Light Skin Tone", []string{"ok_person_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fd", "person gesturing OK: Medium Skin Tone", []string{"ok_person_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fe", "person gesturing OK: Medium-Dark Skin Tone", []string{"ok_person_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f646\U0001f3ff", "person gesturing OK: Dark Skin Tone", []string{"ok_person_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f646\U0001f3ff\u200d\u2640\ufe0f", "woman gesturing OK: Dark Skin Tone", []string{"ok_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f646\U0001f3fb\u200d\u2640\ufe0f", "woman gesturing OK: Light Skin Tone", []string{"ok_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fc\u200d\u2640\ufe0f", "woman gesturing OK: Medium-Light Skin Tone", []string{"ok_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fd\u200d\u2640\ufe0f", "woman gesturing OK: Medium Skin Tone", []string{"ok_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f646\U0001f3fe\u200d\u2640\ufe0f", "woman gesturing OK: Medium-Dark Skin Tone", []string{"ok_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f646\U0001f3ff\u200d\u2640\ufe0f", "woman gesturing OK: Dark Skin Tone", []string{"ok_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f646\U0001f3fb\u200d\u2640\ufe0f", "woman gesturing OK: Light Skin Tone", []string{"ok_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d3\U0001f3fb", "older person: Light Skin Tone", []string{"older_adult_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d3\U0001f3fc", "older person: Medium-Light Skin Tone", []string{"older_adult_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d3\U0001f3fd", "older person: Medium Skin Tone", []string{"older_adult_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d3\U0001f3fe", "older person: Medium-Dark Skin Tone", []string{"older_adult_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d3\U0001f3ff", "older person: Dark Skin Tone", []string{"older_adult_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f474\U0001f3fb", "old man: Light Skin Tone", []string{"older_man_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f474\U0001f3fc", "old man: Medium-Light Skin Tone", []string{"older_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f474\U0001f3fd", "old man: Medium Skin Tone", []string{"older_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f474\U0001f3fe", "old man: Medium-Dark Skin Tone", []string{"older_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f474\U0001f3ff", "old man: Dark Skin Tone", []string{"older_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f474\U0001f3fb", "old man: Light Skin Tone", []string{"older_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f474\U0001f3fc", "old man: Medium-Light Skin Tone", []string{"older_man_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f475\U0001f3ff", "old woman: Dark Skin Tone", []string{"older_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f475\U0001f3fb", "old woman: Light Skin Tone", []string{"older_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f475\U0001f3fc", "old woman: Medium-Light Skin Tone", []string{"older_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f475\U0001f3fd", "old woman: Medium Skin Tone", []string{"older_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f475\U0001f3fe", "old woman: Medium-Dark Skin Tone", []string{"older_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f475\U0001f3ff", "old woman: Dark Skin Tone", []string{"older_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f450\U0001f3fb", "open hands: Light Skin Tone", []string{"open_hands_Light_Skin_Tone"}, "12.0", false}, {"\U0001f450\U0001f3fc", "open hands: Medium-Light Skin Tone", []string{"open_hands_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f450\U0001f3fd", "open hands: Medium Skin Tone", []string{"open_hands_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f450\U0001f3fe", "open hands: Medium-Dark Skin Tone", []string{"open_hands_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f450\U0001f3ff", "open hands: Dark Skin Tone", []string{"open_hands_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f450\U0001f3fb", "open hands: Light Skin Tone", []string{"open_hands_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf3\U0001f3fd", "palm down hand: Medium Skin Tone", []string{"palm_down_hand_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001faf3\U0001f3fe", "palm down hand: Medium-Dark Skin Tone", []string{"palm_down_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf3\U0001f3ff", "palm down hand: Dark Skin Tone", []string{"palm_down_hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf3\U0001f3fb", "palm down hand: Light Skin Tone", []string{"palm_down_hand_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf3\U0001f3fc", "palm down hand: Medium-Light Skin Tone", []string{"palm_down_hand_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf4\U0001f3fb", "palm up hand: Light Skin Tone", []string{"palm_up_hand_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf4\U0001f3fc", "palm up hand: Medium-Light Skin Tone", []string{"palm_up_hand_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf4\U0001f3fd", "palm up hand: Medium Skin Tone", []string{"palm_up_hand_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001faf4\U0001f3fe", "palm up hand: Medium-Dark Skin Tone", []string{"palm_up_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf4\U0001f3ff", "palm up hand: Dark Skin Tone", []string{"palm_up_hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f932\U0001f3fc", "palms up together: Medium-Light Skin Tone", []string{"palms_up_together_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f932\U0001f3fd", "palms up together: Medium Skin Tone", []string{"palms_up_together_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f932\U0001f3fe", "palms up together: Medium-Dark Skin Tone", []string{"palms_up_together_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f932\U0001f3ff", "palms up together: Dark Skin Tone", []string{"palms_up_together_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f932\U0001f3fb", "palms up together: Light Skin Tone", []string{"palms_up_together_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f932\U0001f3fc", "palms up together: Medium-Light Skin Tone", []string{"palms_up_together_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands: Light Skin Tone", []string{"people_holding_hands_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fc\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands: Medium-Light Skin Tone", []string{"people_holding_hands_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fd\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands: Medium Skin Tone", []string{"people_holding_hands_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fe\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands: Medium-Dark Skin Tone", []string{"people_holding_hands_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands: Dark Skin Tone", []string{"people_holding_hands_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f9b2", "person: bald: Dark Skin Tone", []string{"person_bald_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f9b2", "person: bald: Light Skin Tone", []string{"person_bald_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9b2", "person: bald: Medium-Light Skin Tone", []string{"person_bald_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9b2", "person: bald: Medium Skin Tone", []string{"person_bald_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f9b2", "person: bald: Medium-Dark Skin Tone", []string{"person_bald_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f9b1", "person: curly hair: Light Skin Tone", []string{"person_curly_hair_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f9b1", "person: curly hair: Medium-Light Skin Tone", []string{"person_curly_hair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9b1", "person: curly hair: Medium Skin Tone", []string{"person_curly_hair_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f9b1", "person: curly hair: Medium-Dark Skin Tone", []string{"person_curly_hair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f9b1", "person: curly hair: Dark Skin Tone", []string{"person_curly_hair_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f9b1", "person: curly hair: Light Skin Tone", []string{"person_curly_hair_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fc\u200d\U0001f9b1", "person: curly hair: Medium-Light Skin Tone", []string{"person_curly_hair_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fe\u200d\U0001f9bd", "person in manual wheelchair: Medium-Dark Skin Tone", []string{"person_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f9bd", "person in manual wheelchair: Dark Skin Tone", []string{"person_in_manual_wheelchair_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f37c", "person feeding baby: Light Skin Tone", []string{"person_feeding_baby_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f37c", "person feeding baby: Medium-Light Skin Tone", []string{"person_feeding_baby_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fd\u200d\U0001f37c", "person feeding baby: Medium Skin Tone", []string{"person_feeding_baby_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f37c", "person feeding baby: Medium-Dark Skin Tone", []string{"person_feeding_baby_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f37c", "person feeding baby: Dark Skin Tone", []string{"person_feeding_baby_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f9bd", "person in manual wheelchair: Light Skin Tone", []string{"person_in_manual_wheelchair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9bd", "person in manual wheelchair: Medium-Light Skin Tone", []string{"person_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9bd", "person in manual wheelchair: Medium Skin Tone", []string{"person_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fe\u200d\U0001f9bc", "person in motorized wheelchair: Medium-Dark Skin Tone", []string{"person_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f9bc", "person in motorized wheelchair: Dark Skin Tone", []string{"person_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f9bd", "person in manual wheelchair: Medium-Dark Skin Tone", []string{"person_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f9bd", "person in manual wheelchair: Dark Skin Tone", []string{"person_in_manual_wheelchair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f9bc", "person in motorized wheelchair: Light Skin Tone", []string{"person_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9bc", "person in motorized wheelchair: Medium-Light Skin Tone", []string{"person_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9bc", "person in motorized wheelchair: Medium Skin Tone", []string{"person_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f9bc", "person in motorized wheelchair: Medium-Dark Skin Tone", []string{"person_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f9bc", "person in motorized wheelchair: Dark Skin Tone", []string{"person_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fb", "person in tuxedo: Light Skin Tone", []string{"person_in_tuxedo_Light_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fc", "person in tuxedo: Medium-Light Skin Tone", []string{"person_in_tuxedo_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fd", "person in tuxedo: Medium Skin Tone", []string{"person_in_tuxedo_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fe", "person in tuxedo: Medium-Dark Skin Tone", []string{"person_in_tuxedo_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3ff", "person in tuxedo: Dark Skin Tone", []string{"person_in_tuxedo_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f9b0", "person: red hair: Dark Skin Tone", []string{"person_red_hair_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f9b0", "person: red hair: Light Skin Tone", []string{"person_red_hair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9b0", "person: red hair: Medium-Light Skin Tone", []string{"person_red_hair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9b0", "person: red hair: Medium Skin Tone", []string{"person_red_hair_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f9b0", "person: red hair: Medium-Dark Skin Tone", []string{"person_red_hair_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f9b3", "person: white hair: Light Skin Tone", []string{"person_white_hair_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f9b0", "person: red hair: Dark Skin Tone", []string{"person_red_hair_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f9b0", "person: red hair: Light Skin Tone", []string{"person_red_hair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9b3", "person: white hair: Medium-Light Skin Tone", []string{"person_white_hair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9b3", "person: white hair: Medium Skin Tone", []string{"person_white_hair_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f9b3", "person: white hair: Medium-Dark Skin Tone", []string{"person_white_hair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f9b3", "person: white hair: Dark Skin Tone", []string{"person_white_hair_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f9b3", "person: white hair: Light Skin Tone", []string{"person_white_hair_Light_Skin_Tone"}, "12.0", false}, + {"\U0001fac5\U0001f3ff", "person with crown: Dark Skin Tone", []string{"person_with_crown_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001fac5\U0001f3fb", "person with crown: Light Skin Tone", []string{"person_with_crown_Light_Skin_Tone"}, "12.0", false}, + {"\U0001fac5\U0001f3fc", "person with crown: Medium-Light Skin Tone", []string{"person_with_crown_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001fac5\U0001f3fd", "person with crown: Medium Skin Tone", []string{"person_with_crown_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001fac5\U0001f3fe", "person with crown: Medium-Dark Skin Tone", []string{"person_with_crown_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f9af", "person with white cane: Medium-Dark Skin Tone", []string{"person_with_probing_cane_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f9af", "person with white cane: Dark Skin Tone", []string{"person_with_probing_cane_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f9af", "person with white cane: Light Skin Tone", []string{"person_with_probing_cane_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9af", "person with white cane: Medium-Light Skin Tone", []string{"person_with_probing_cane_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9af", "person with white cane: Medium Skin Tone", []string{"person_with_probing_cane_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fe\u200d\U0001f9af", "person with white cane: Medium-Dark Skin Tone", []string{"person_with_probing_cane_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f9af", "person with white cane: Dark Skin Tone", []string{"person_with_probing_cane_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fe", "person wearing turban: Medium-Dark Skin Tone", []string{"person_with_turban_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3ff", "person wearing turban: Dark Skin Tone", []string{"person_with_turban_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fb", "person wearing turban: Light Skin Tone", []string{"person_with_turban_Light_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fc", "person wearing turban: Medium-Light Skin Tone", []string{"person_with_turban_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fd", "person wearing turban: Medium Skin Tone", []string{"person_with_turban_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fc", "person with veil: Medium-Light Skin Tone", []string{"person_with_veil_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fd", "person with veil: Medium Skin Tone", []string{"person_with_veil_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fe", "person with veil: Medium-Dark Skin Tone", []string{"person_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3ff", "person with veil: Dark Skin Tone", []string{"person_with_veil_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fb", "person with veil: Light Skin Tone", []string{"person_with_veil_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f470\U0001f3fc", "person with veil: Medium-Light Skin Tone", []string{"person_with_veil_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\u2708\ufe0f", "pilot: Light Skin Tone", []string{"pilot_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\u2708\ufe0f", "pilot: Medium-Light Skin Tone", []string{"pilot_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\u2708\ufe0f", "pilot: Medium Skin Tone", []string{"pilot_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\u2708\ufe0f", "pilot: Medium-Dark Skin Tone", []string{"pilot_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\u2708\ufe0f", "pilot: Dark Skin Tone", []string{"pilot_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\u2708\ufe0f", "pilot: Light Skin Tone", []string{"pilot_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f90c\U0001f3fb", "pinched fingers: Light Skin Tone", []string{"pinched_fingers_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f90c\U0001f3fc", "pinched fingers: Medium-Light Skin Tone", []string{"pinched_fingers_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f90c\U0001f3fd", "pinched fingers: Medium Skin Tone", []string{"pinched_fingers_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f90c\U0001f3fe", "pinched fingers: Medium-Dark Skin Tone", []string{"pinched_fingers_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f90c\U0001f3ff", "pinched fingers: Dark Skin Tone", []string{"pinched_fingers_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f90f\U0001f3fe", "pinching hand: Medium-Dark Skin Tone", []string{"pinching_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f90f\U0001f3ff", "pinching hand: Dark Skin Tone", []string{"pinching_hand_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f90f\U0001f3fb", "pinching hand: Light Skin Tone", []string{"pinching_hand_Light_Skin_Tone"}, "12.0", false}, {"\U0001f90f\U0001f3fc", "pinching hand: Medium-Light Skin Tone", []string{"pinching_hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f90f\U0001f3fd", "pinching hand: Medium Skin Tone", []string{"pinching_hand_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f447\U0001f3ff", "backhand index pointing down: Dark Skin Tone", []string{"point_down_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f447\U0001f3fb", "backhand index pointing down: Light Skin Tone", []string{"point_down_Light_Skin_Tone"}, "12.0", false}, {"\U0001f447\U0001f3fc", "backhand index pointing down: Medium-Light Skin Tone", []string{"point_down_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f447\U0001f3fd", "backhand index pointing down: Medium Skin Tone", []string{"point_down_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f447\U0001f3fe", "backhand index pointing down: Medium-Dark Skin Tone", []string{"point_down_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f448\U0001f3fe", "backhand index pointing left: Medium-Dark Skin Tone", []string{"point_left_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f447\U0001f3ff", "backhand index pointing down: Dark Skin Tone", []string{"point_down_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f448\U0001f3ff", "backhand index pointing left: Dark Skin Tone", []string{"point_left_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f448\U0001f3fb", "backhand index pointing left: Light Skin Tone", []string{"point_left_Light_Skin_Tone"}, "12.0", false}, {"\U0001f448\U0001f3fc", "backhand index pointing left: Medium-Light Skin Tone", []string{"point_left_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f448\U0001f3fd", "backhand index pointing left: Medium Skin Tone", []string{"point_left_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f449\U0001f3fb", "backhand index pointing right: Light Skin Tone", []string{"point_right_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f449\U0001f3fc", "backhand index pointing right: Medium-Light Skin Tone", []string{"point_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f448\U0001f3fe", "backhand index pointing left: Medium-Dark Skin Tone", []string{"point_left_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f449\U0001f3fd", "backhand index pointing right: Medium Skin Tone", []string{"point_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f449\U0001f3fe", "backhand index pointing right: Medium-Dark Skin Tone", []string{"point_right_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f449\U0001f3ff", "backhand index pointing right: Dark Skin Tone", []string{"point_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f449\U0001f3fb", "backhand index pointing right: Light Skin Tone", []string{"point_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f449\U0001f3fc", "backhand index pointing right: Medium-Light Skin Tone", []string{"point_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\u261d\U0001f3fb\ufe0f", "index pointing up: Light Skin Tone", []string{"point_up_Light_Skin_Tone"}, "12.0", false}, {"\u261d\U0001f3fc\ufe0f", "index pointing up: Medium-Light Skin Tone", []string{"point_up_Medium-Light_Skin_Tone"}, "12.0", false}, {"\u261d\U0001f3fd\ufe0f", "index pointing up: Medium Skin Tone", []string{"point_up_Medium_Skin_Tone"}, "12.0", false}, {"\u261d\U0001f3fe\ufe0f", "index pointing up: Medium-Dark Skin Tone", []string{"point_up_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\u261d\U0001f3ff\ufe0f", "index pointing up: Dark Skin Tone", []string{"point_up_Dark_Skin_Tone"}, "12.0", false}, - {"\u261d\U0001f3fb\ufe0f", "index pointing up: Light Skin Tone", []string{"point_up_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f446\U0001f3fb", "backhand index pointing up: Light Skin Tone", []string{"point_up_2_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f446\U0001f3fc", "backhand index pointing up: Medium-Light Skin Tone", []string{"point_up_2_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f446\U0001f3fd", "backhand index pointing up: Medium Skin Tone", []string{"point_up_2_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f446\U0001f3fe", "backhand index pointing up: Medium-Dark Skin Tone", []string{"point_up_2_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f446\U0001f3ff", "backhand index pointing up: Dark Skin Tone", []string{"point_up_2_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f446\U0001f3fb", "backhand index pointing up: Light Skin Tone", []string{"point_up_2_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f446\U0001f3fc", "backhand index pointing up: Medium-Light Skin Tone", []string{"point_up_2_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f46e\U0001f3fe", "police officer: Medium-Dark Skin Tone", []string{"police_officer_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3ff", "police officer: Dark Skin Tone", []string{"police_officer_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fb", "police officer: Light Skin Tone", []string{"police_officer_Light_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fc", "police officer: Medium-Light Skin Tone", []string{"police_officer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fd", "police officer: Medium Skin Tone", []string{"police_officer_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f46e\U0001f3fe", "police officer: Medium-Dark Skin Tone", []string{"police_officer_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f46e\U0001f3fb\u200d\u2642\ufe0f", "man police officer: Light Skin Tone", []string{"policeman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f46e\U0001f3fc\u200d\u2642\ufe0f", "man police officer: Medium-Light Skin Tone", []string{"policeman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fd\u200d\u2642\ufe0f", "man police officer: Medium Skin Tone", []string{"policeman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fe\u200d\u2642\ufe0f", "man police officer: Medium-Dark Skin Tone", []string{"policeman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3ff\u200d\u2642\ufe0f", "man police officer: Dark Skin Tone", []string{"policeman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f46e\U0001f3fb\u200d\u2642\ufe0f", "man police officer: Light Skin Tone", []string{"policeman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f46e\U0001f3fc\u200d\u2642\ufe0f", "man police officer: Medium-Light Skin Tone", []string{"policeman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f46e\U0001f3fc\u200d\u2640\ufe0f", "woman police officer: Medium-Light Skin Tone", []string{"policewoman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fd\u200d\u2640\ufe0f", "woman police officer: Medium Skin Tone", []string{"policewoman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fe\u200d\u2640\ufe0f", "woman police officer: Medium-Dark Skin Tone", []string{"policewoman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3ff\u200d\u2640\ufe0f", "woman police officer: Dark Skin Tone", []string{"policewoman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46e\U0001f3fb\u200d\u2640\ufe0f", "woman police officer: Light Skin Tone", []string{"policewoman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f46e\U0001f3fc\u200d\u2640\ufe0f", "woman police officer: Medium-Light Skin Tone", []string{"policewoman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fb", "person pouting: Light Skin Tone", []string{"pouting_face_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fc", "person pouting: Medium-Light Skin Tone", []string{"pouting_face_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fd", "person pouting: Medium Skin Tone", []string{"pouting_face_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fe", "person pouting: Medium-Dark Skin Tone", []string{"pouting_face_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3ff", "person pouting: Dark Skin Tone", []string{"pouting_face_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f64e\U0001f3fe\u200d\u2642\ufe0f", "man pouting: Medium-Dark Skin Tone", []string{"pouting_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f64e\U0001f3ff\u200d\u2642\ufe0f", "man pouting: Dark Skin Tone", []string{"pouting_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fb\u200d\u2642\ufe0f", "man pouting: Light Skin Tone", []string{"pouting_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fc\u200d\u2642\ufe0f", "man pouting: Medium-Light Skin Tone", []string{"pouting_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fd\u200d\u2642\ufe0f", "man pouting: Medium Skin Tone", []string{"pouting_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f64e\U0001f3fe\u200d\u2642\ufe0f", "man pouting: Medium-Dark Skin Tone", []string{"pouting_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f64e\U0001f3ff\u200d\u2642\ufe0f", "man pouting: Dark Skin Tone", []string{"pouting_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fb\u200d\u2640\ufe0f", "woman pouting: Light Skin Tone", []string{"pouting_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fc\u200d\u2640\ufe0f", "woman pouting: Medium-Light Skin Tone", []string{"pouting_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fd\u200d\u2640\ufe0f", "woman pouting: Medium Skin Tone", []string{"pouting_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3fe\u200d\u2640\ufe0f", "woman pouting: Medium-Dark Skin Tone", []string{"pouting_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64e\U0001f3ff\u200d\u2640\ufe0f", "woman pouting: Dark Skin Tone", []string{"pouting_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f64f\U0001f3fe", "folded hands: Medium-Dark Skin Tone", []string{"pray_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f64f\U0001f3ff", "folded hands: Dark Skin Tone", []string{"pray_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64f\U0001f3fb", "folded hands: Light Skin Tone", []string{"pray_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64f\U0001f3fc", "folded hands: Medium-Light Skin Tone", []string{"pray_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64f\U0001f3fd", "folded hands: Medium Skin Tone", []string{"pray_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f64f\U0001f3fe", "folded hands: Medium-Dark Skin Tone", []string{"pray_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f64f\U0001f3ff", "folded hands: Dark Skin Tone", []string{"pray_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001fac3\U0001f3fb", "pregnant man: Light Skin Tone", []string{"pregnant_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001fac3\U0001f3fc", "pregnant man: Medium-Light Skin Tone", []string{"pregnant_man_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001fac3\U0001f3fd", "pregnant man: Medium Skin Tone", []string{"pregnant_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001fac3\U0001f3fe", "pregnant man: Medium-Dark Skin Tone", []string{"pregnant_man_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001fac3\U0001f3ff", "pregnant man: Dark Skin Tone", []string{"pregnant_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001fac4\U0001f3fc", "pregnant person: Medium-Light Skin Tone", []string{"pregnant_person_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001fac4\U0001f3fd", "pregnant person: Medium Skin Tone", []string{"pregnant_person_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001fac4\U0001f3fe", "pregnant person: Medium-Dark Skin Tone", []string{"pregnant_person_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001fac4\U0001f3ff", "pregnant person: Dark Skin Tone", []string{"pregnant_person_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001fac4\U0001f3fb", "pregnant person: Light Skin Tone", []string{"pregnant_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f930\U0001f3fb", "pregnant woman: Light Skin Tone", []string{"pregnant_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f930\U0001f3fc", "pregnant woman: Medium-Light Skin Tone", []string{"pregnant_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f930\U0001f3fd", "pregnant woman: Medium Skin Tone", []string{"pregnant_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f930\U0001f3fe", "pregnant woman: Medium-Dark Skin Tone", []string{"pregnant_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f930\U0001f3ff", "pregnant woman: Dark Skin Tone", []string{"pregnant_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f934\U0001f3ff", "prince: Dark Skin Tone", []string{"prince_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f934\U0001f3fb", "prince: Light Skin Tone", []string{"prince_Light_Skin_Tone"}, "12.0", false}, {"\U0001f934\U0001f3fc", "prince: Medium-Light Skin Tone", []string{"prince_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f934\U0001f3fd", "prince: Medium Skin Tone", []string{"prince_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f934\U0001f3fe", "prince: Medium-Dark Skin Tone", []string{"prince_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f934\U0001f3ff", "prince: Dark Skin Tone", []string{"prince_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f934\U0001f3fb", "prince: Light Skin Tone", []string{"prince_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f478\U0001f3ff", "princess: Dark Skin Tone", []string{"princess_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f478\U0001f3fb", "princess: Light Skin Tone", []string{"princess_Light_Skin_Tone"}, "12.0", false}, {"\U0001f478\U0001f3fc", "princess: Medium-Light Skin Tone", []string{"princess_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f478\U0001f3fd", "princess: Medium Skin Tone", []string{"princess_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f478\U0001f3fe", "princess: Medium-Dark Skin Tone", []string{"princess_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f478\U0001f3ff", "princess: Dark Skin Tone", []string{"princess_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f478\U0001f3fb", "princess: Light Skin Tone", []string{"princess_Light_Skin_Tone"}, "12.0", false}, {"\U0001f91a\U0001f3ff", "raised back of hand: Dark Skin Tone", []string{"raised_back_of_hand_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f91a\U0001f3fb", "raised back of hand: Light Skin Tone", []string{"raised_back_of_hand_Light_Skin_Tone"}, "12.0", false}, {"\U0001f91a\U0001f3fc", "raised back of hand: Medium-Light Skin Tone", []string{"raised_back_of_hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f91a\U0001f3fd", "raised back of hand: Medium Skin Tone", []string{"raised_back_of_hand_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f91a\U0001f3fe", "raised back of hand: Medium-Dark Skin Tone", []string{"raised_back_of_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f590\U0001f3fb\ufe0f", "hand with fingers splayed: Light Skin Tone", []string{"raised_hand_with_fingers_splayed_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f590\U0001f3fc\ufe0f", "hand with fingers splayed: Medium-Light Skin Tone", []string{"raised_hand_with_fingers_splayed_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f590\U0001f3fd\ufe0f", "hand with fingers splayed: Medium Skin Tone", []string{"raised_hand_with_fingers_splayed_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f590\U0001f3fe\ufe0f", "hand with fingers splayed: Medium-Dark Skin Tone", []string{"raised_hand_with_fingers_splayed_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f590\U0001f3ff\ufe0f", "hand with fingers splayed: Dark Skin Tone", []string{"raised_hand_with_fingers_splayed_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f590\U0001f3fb\ufe0f", "hand with fingers splayed: Light Skin Tone", []string{"raised_hand_with_fingers_splayed_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f590\U0001f3fc\ufe0f", "hand with fingers splayed: Medium-Light Skin Tone", []string{"raised_hand_with_fingers_splayed_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64c\U0001f3fb", "raising hands: Light Skin Tone", []string{"raised_hands_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64c\U0001f3fc", "raising hands: Medium-Light Skin Tone", []string{"raised_hands_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64c\U0001f3fd", "raising hands: Medium Skin Tone", []string{"raised_hands_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64c\U0001f3fe", "raising hands: Medium-Dark Skin Tone", []string{"raised_hands_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64c\U0001f3ff", "raising hands: Dark Skin Tone", []string{"raised_hands_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f64b\U0001f3ff", "person raising hand: Dark Skin Tone", []string{"raising_hand_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f64b\U0001f3fb", "person raising hand: Light Skin Tone", []string{"raising_hand_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fc", "person raising hand: Medium-Light Skin Tone", []string{"raising_hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fd", "person raising hand: Medium Skin Tone", []string{"raising_hand_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fe", "person raising hand: Medium-Dark Skin Tone", []string{"raising_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f64b\U0001f3ff\u200d\u2642\ufe0f", "man raising hand: Dark Skin Tone", []string{"raising_hand_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f64b\U0001f3ff", "person raising hand: Dark Skin Tone", []string{"raising_hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f64b\U0001f3fb", "person raising hand: Light Skin Tone", []string{"raising_hand_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fb\u200d\u2642\ufe0f", "man raising hand: Light Skin Tone", []string{"raising_hand_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fc\u200d\u2642\ufe0f", "man raising hand: Medium-Light Skin Tone", []string{"raising_hand_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fd\u200d\u2642\ufe0f", "man raising hand: Medium Skin Tone", []string{"raising_hand_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fe\u200d\u2642\ufe0f", "man raising hand: Medium-Dark Skin Tone", []string{"raising_hand_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f64b\U0001f3fb\u200d\u2640\ufe0f", "woman raising hand: Light Skin Tone", []string{"raising_hand_woman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f64b\U0001f3fc\u200d\u2640\ufe0f", "woman raising hand: Medium-Light Skin Tone", []string{"raising_hand_woman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f64b\U0001f3ff\u200d\u2642\ufe0f", "man raising hand: Dark Skin Tone", []string{"raising_hand_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fd\u200d\u2640\ufe0f", "woman raising hand: Medium Skin Tone", []string{"raising_hand_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3fe\u200d\u2640\ufe0f", "woman raising hand: Medium-Dark Skin Tone", []string{"raising_hand_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f64b\U0001f3ff\u200d\u2640\ufe0f", "woman raising hand: Dark Skin Tone", []string{"raising_hand_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f64b\U0001f3fb\u200d\u2640\ufe0f", "woman raising hand: Light Skin Tone", []string{"raising_hand_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f64b\U0001f3fc\u200d\u2640\ufe0f", "woman raising hand: Medium-Light Skin Tone", []string{"raising_hand_woman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f9b0", "man: red hair: Light Skin Tone", []string{"red_haired_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f9b0", "man: red hair: Medium-Light Skin Tone", []string{"red_haired_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f9b0", "man: red hair: Medium Skin Tone", []string{"red_haired_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f9b0", "man: red hair: Medium-Dark Skin Tone", []string{"red_haired_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f9b0", "man: red hair: Dark Skin Tone", []string{"red_haired_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fb\u200d\U0001f9b0", "man: red hair: Light Skin Tone", []string{"red_haired_man_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f9b0", "man: red hair: Medium-Light Skin Tone", []string{"red_haired_man_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\U0001f9b0", "woman: red hair: Medium-Light Skin Tone", []string{"red_haired_woman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fd\u200d\U0001f9b0", "woman: red hair: Medium Skin Tone", []string{"red_haired_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f9b0", "woman: red hair: Medium-Dark Skin Tone", []string{"red_haired_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f9b0", "woman: red hair: Dark Skin Tone", []string{"red_haired_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f9b0", "woman: red hair: Light Skin Tone", []string{"red_haired_woman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fc\u200d\U0001f9b0", "woman: red hair: Medium-Light Skin Tone", []string{"red_haired_woman_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fd\u200d\U0001f9b0", "woman: red hair: Medium Skin Tone", []string{"red_haired_woman_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f6a3\U0001f3fb", "person rowing boat: Light Skin Tone", []string{"rowboat_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6a3\U0001f3fc", "person rowing boat: Medium-Light Skin Tone", []string{"rowboat_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf1\U0001f3fb", "rightwards hand: Light Skin Tone", []string{"rightwards_hand_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf1\U0001f3fc", "rightwards hand: Medium-Light Skin Tone", []string{"rightwards_hand_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf1\U0001f3fd", "rightwards hand: Medium Skin Tone", []string{"rightwards_hand_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001faf1\U0001f3fe", "rightwards hand: Medium-Dark Skin Tone", []string{"rightwards_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf1\U0001f3ff", "rightwards hand: Dark Skin Tone", []string{"rightwards_hand_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fd", "person rowing boat: Medium Skin Tone", []string{"rowboat_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fe", "person rowing boat: Medium-Dark Skin Tone", []string{"rowboat_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3ff", "person rowing boat: Dark Skin Tone", []string{"rowboat_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6a3\U0001f3ff\u200d\u2642\ufe0f", "man rowing boat: Dark Skin Tone", []string{"rowing_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6a3\U0001f3fb\u200d\u2642\ufe0f", "man rowing boat: Light Skin Tone", []string{"rowing_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6a3\U0001f3fb", "person rowing boat: Light Skin Tone", []string{"rowboat_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6a3\U0001f3fc", "person rowing boat: Medium-Light Skin Tone", []string{"rowboat_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fc\u200d\u2642\ufe0f", "man rowing boat: Medium-Light Skin Tone", []string{"rowing_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fd\u200d\u2642\ufe0f", "man rowing boat: Medium Skin Tone", []string{"rowing_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fe\u200d\u2642\ufe0f", "man rowing boat: Medium-Dark Skin Tone", []string{"rowing_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6a3\U0001f3fb\u200d\u2640\ufe0f", "woman rowing boat: Light Skin Tone", []string{"rowing_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6a3\U0001f3ff\u200d\u2642\ufe0f", "man rowing boat: Dark Skin Tone", []string{"rowing_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6a3\U0001f3fb\u200d\u2642\ufe0f", "man rowing boat: Light Skin Tone", []string{"rowing_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fc\u200d\u2640\ufe0f", "woman rowing boat: Medium-Light Skin Tone", []string{"rowing_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fd\u200d\u2640\ufe0f", "woman rowing boat: Medium Skin Tone", []string{"rowing_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3fe\u200d\u2640\ufe0f", "woman rowing boat: Medium-Dark Skin Tone", []string{"rowing_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6a3\U0001f3ff\u200d\u2640\ufe0f", "woman rowing boat: Dark Skin Tone", []string{"rowing_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6a3\U0001f3fb\u200d\u2640\ufe0f", "woman rowing boat: Light Skin Tone", []string{"rowing_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c3\U0001f3fb", "person running: Light Skin Tone", []string{"runner_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c3\U0001f3fc", "person running: Medium-Light Skin Tone", []string{"runner_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c3\U0001f3fd", "person running: Medium Skin Tone", []string{"runner_Medium_Skin_Tone"}, "12.0", false}, @@ -2755,71 +2956,71 @@ var GemojiData = Gemoji{ {"\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f", "woman running: Medium-Light Skin Tone", []string{"running_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f", "woman running: Medium Skin Tone", []string{"running_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f", "woman running: Medium-Dark Skin Tone", []string{"running_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f385\U0001f3fe", "Santa Claus: Medium-Dark Skin Tone", []string{"santa_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f385\U0001f3ff", "Santa Claus: Dark Skin Tone", []string{"santa_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f385\U0001f3fb", "Santa Claus: Light Skin Tone", []string{"santa_Light_Skin_Tone"}, "12.0", false}, {"\U0001f385\U0001f3fc", "Santa Claus: Medium-Light Skin Tone", []string{"santa_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f385\U0001f3fd", "Santa Claus: Medium Skin Tone", []string{"santa_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f385\U0001f3fe", "Santa Claus: Medium-Dark Skin Tone", []string{"santa_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f385\U0001f3ff", "Santa Claus: Dark Skin Tone", []string{"santa_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d6\U0001f3fb\u200d\u2642\ufe0f", "man in steamy room: Light Skin Tone", []string{"sauna_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fc\u200d\u2642\ufe0f", "man in steamy room: Medium-Light Skin Tone", []string{"sauna_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fd\u200d\u2642\ufe0f", "man in steamy room: Medium Skin Tone", []string{"sauna_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fe\u200d\u2642\ufe0f", "man in steamy room: Medium-Dark Skin Tone", []string{"sauna_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3ff\u200d\u2642\ufe0f", "man in steamy room: Dark Skin Tone", []string{"sauna_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d6\U0001f3fb\u200d\u2642\ufe0f", "man in steamy room: Light Skin Tone", []string{"sauna_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d6\U0001f3fe", "person in steamy room: Medium-Dark Skin Tone", []string{"sauna_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3ff", "person in steamy room: Dark Skin Tone", []string{"sauna_person_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fb", "person in steamy room: Light Skin Tone", []string{"sauna_person_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fc", "person in steamy room: Medium-Light Skin Tone", []string{"sauna_person_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fd", "person in steamy room: Medium Skin Tone", []string{"sauna_person_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d6\U0001f3fe", "person in steamy room: Medium-Dark Skin Tone", []string{"sauna_person_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d6\U0001f3fc\u200d\u2640\ufe0f", "woman in steamy room: Medium-Light Skin Tone", []string{"sauna_woman_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d6\U0001f3fd\u200d\u2640\ufe0f", "woman in steamy room: Medium Skin Tone", []string{"sauna_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fe\u200d\u2640\ufe0f", "woman in steamy room: Medium-Dark Skin Tone", []string{"sauna_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3ff\u200d\u2640\ufe0f", "woman in steamy room: Dark Skin Tone", []string{"sauna_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d6\U0001f3fb\u200d\u2640\ufe0f", "woman in steamy room: Light Skin Tone", []string{"sauna_woman_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d6\U0001f3fc\u200d\u2640\ufe0f", "woman in steamy room: Medium-Light Skin Tone", []string{"sauna_woman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d6\U0001f3fd\u200d\u2640\ufe0f", "woman in steamy room: Medium Skin Tone", []string{"sauna_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f52c", "scientist: Light Skin Tone", []string{"scientist_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f52c", "scientist: Medium-Light Skin Tone", []string{"scientist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f52c", "scientist: Medium Skin Tone", []string{"scientist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f52c", "scientist: Medium-Dark Skin Tone", []string{"scientist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f52c", "scientist: Dark Skin Tone", []string{"scientist_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f933\U0001f3fb", "selfie: Light Skin Tone", []string{"selfie_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f933\U0001f3fc", "selfie: Medium-Light Skin Tone", []string{"selfie_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f933\U0001f3fd", "selfie: Medium Skin Tone", []string{"selfie_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f933\U0001f3fe", "selfie: Medium-Dark Skin Tone", []string{"selfie_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f933\U0001f3ff", "selfie: Dark Skin Tone", []string{"selfie_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f937\U0001f3fe", "person shrugging: Medium-Dark Skin Tone", []string{"shrug_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f937\U0001f3ff", "person shrugging: Dark Skin Tone", []string{"shrug_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f933\U0001f3fb", "selfie: Light Skin Tone", []string{"selfie_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f933\U0001f3fc", "selfie: Medium-Light Skin Tone", []string{"selfie_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fb", "person shrugging: Light Skin Tone", []string{"shrug_Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fc", "person shrugging: Medium-Light Skin Tone", []string{"shrug_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fd", "person shrugging: Medium Skin Tone", []string{"shrug_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f3a4", "singer: Light Skin Tone", []string{"singer_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f937\U0001f3fe", "person shrugging: Medium-Dark Skin Tone", []string{"shrug_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f937\U0001f3ff", "person shrugging: Dark Skin Tone", []string{"shrug_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f3a4", "singer: Medium-Light Skin Tone", []string{"singer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f3a4", "singer: Medium Skin Tone", []string{"singer_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f3a4", "singer: Medium-Dark Skin Tone", []string{"singer_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3ff\u200d\U0001f3a4", "singer: Dark Skin Tone", []string{"singer_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f3a4", "singer: Light Skin Tone", []string{"singer_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6cc\U0001f3fb", "person in bed: Light Skin Tone", []string{"sleeping_bed_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6cc\U0001f3fc", "person in bed: Medium-Light Skin Tone", []string{"sleeping_bed_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6cc\U0001f3fd", "person in bed: Medium Skin Tone", []string{"sleeping_bed_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6cc\U0001f3fe", "person in bed: Medium-Dark Skin Tone", []string{"sleeping_bed_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6cc\U0001f3ff", "person in bed: Dark Skin Tone", []string{"sleeping_bed_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6cc\U0001f3fb", "person in bed: Light Skin Tone", []string{"sleeping_bed_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6cc\U0001f3fc", "person in bed: Medium-Light Skin Tone", []string{"sleeping_bed_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c2\U0001f3fd", "snowboarder: Medium Skin Tone", []string{"snowboarder_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3c2\U0001f3fe", "snowboarder: Medium-Dark Skin Tone", []string{"snowboarder_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3c2\U0001f3ff", "snowboarder: Dark Skin Tone", []string{"snowboarder_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3c2\U0001f3fb", "snowboarder: Light Skin Tone", []string{"snowboarder_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c2\U0001f3fc", "snowboarder: Medium-Light Skin Tone", []string{"snowboarder_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f3c2\U0001f3fd", "snowboarder: Medium Skin Tone", []string{"snowboarder_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9cd\U0001f3ff\u200d\u2642\ufe0f", "man standing: Dark Skin Tone", []string{"standing_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cd\U0001f3fb\u200d\u2642\ufe0f", "man standing: Light Skin Tone", []string{"standing_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fc\u200d\u2642\ufe0f", "man standing: Medium-Light Skin Tone", []string{"standing_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fd\u200d\u2642\ufe0f", "man standing: Medium Skin Tone", []string{"standing_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fe\u200d\u2642\ufe0f", "man standing: Medium-Dark Skin Tone", []string{"standing_man_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cd\U0001f3fb", "person standing: Light Skin Tone", []string{"standing_person_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9cd\U0001f3fc", "person standing: Medium-Light Skin Tone", []string{"standing_person_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9cd\U0001f3ff\u200d\u2642\ufe0f", "man standing: Dark Skin Tone", []string{"standing_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9cd\U0001f3fb\u200d\u2642\ufe0f", "man standing: Light Skin Tone", []string{"standing_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fd", "person standing: Medium Skin Tone", []string{"standing_person_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fe", "person standing: Medium-Dark Skin Tone", []string{"standing_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3ff", "person standing: Dark Skin Tone", []string{"standing_person_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9cd\U0001f3ff\u200d\u2640\ufe0f", "woman standing: Dark Skin Tone", []string{"standing_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9cd\U0001f3fb", "person standing: Light Skin Tone", []string{"standing_person_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9cd\U0001f3fc", "person standing: Medium-Light Skin Tone", []string{"standing_person_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fb\u200d\u2640\ufe0f", "woman standing: Light Skin Tone", []string{"standing_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fc\u200d\u2640\ufe0f", "woman standing: Medium-Light Skin Tone", []string{"standing_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fd\u200d\u2640\ufe0f", "woman standing: Medium Skin Tone", []string{"standing_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9cd\U0001f3fe\u200d\u2640\ufe0f", "woman standing: Medium-Dark Skin Tone", []string{"standing_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9cd\U0001f3ff\u200d\u2640\ufe0f", "woman standing: Dark Skin Tone", []string{"standing_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f393", "student: Light Skin Tone", []string{"student_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f393", "student: Medium-Light Skin Tone", []string{"student_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f393", "student: Medium Skin Tone", []string{"student_Medium_Skin_Tone"}, "12.0", false}, @@ -2835,16 +3036,16 @@ var GemojiData = Gemoji{ {"\U0001f9b8\U0001f3fd\u200d\u2642\ufe0f", "man superhero: Medium Skin Tone", []string{"superhero_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9b8\U0001f3fe\u200d\u2642\ufe0f", "man superhero: Medium-Dark Skin Tone", []string{"superhero_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b8\U0001f3ff\u200d\u2642\ufe0f", "man superhero: Dark Skin Tone", []string{"superhero_man_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9b8\U0001f3fe\u200d\u2640\ufe0f", "woman superhero: Medium-Dark Skin Tone", []string{"superhero_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9b8\U0001f3ff\u200d\u2640\ufe0f", "woman superhero: Dark Skin Tone", []string{"superhero_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b8\U0001f3fb\u200d\u2640\ufe0f", "woman superhero: Light Skin Tone", []string{"superhero_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b8\U0001f3fc\u200d\u2640\ufe0f", "woman superhero: Medium-Light Skin Tone", []string{"superhero_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b8\U0001f3fd\u200d\u2640\ufe0f", "woman superhero: Medium Skin Tone", []string{"superhero_woman_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9b8\U0001f3fe\u200d\u2640\ufe0f", "woman superhero: Medium-Dark Skin Tone", []string{"superhero_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9b8\U0001f3ff\u200d\u2640\ufe0f", "woman superhero: Dark Skin Tone", []string{"superhero_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9b9\U0001f3fe", "supervillain: Medium-Dark Skin Tone", []string{"supervillain_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b9\U0001f3ff", "supervillain: Dark Skin Tone", []string{"supervillain_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b9\U0001f3fb", "supervillain: Light Skin Tone", []string{"supervillain_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b9\U0001f3fc", "supervillain: Medium-Light Skin Tone", []string{"supervillain_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b9\U0001f3fd", "supervillain: Medium Skin Tone", []string{"supervillain_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9b9\U0001f3fe", "supervillain: Medium-Dark Skin Tone", []string{"supervillain_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9b9\U0001f3fb\u200d\u2642\ufe0f", "man supervillain: Light Skin Tone", []string{"supervillain_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b9\U0001f3fc\u200d\u2642\ufe0f", "man supervillain: Medium-Light Skin Tone", []string{"supervillain_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9b9\U0001f3fd\u200d\u2642\ufe0f", "man supervillain: Medium Skin Tone", []string{"supervillain_man_Medium_Skin_Tone"}, "12.0", false}, @@ -2860,36 +3061,36 @@ var GemojiData = Gemoji{ {"\U0001f3c4\U0001f3fd", "person surfing: Medium Skin Tone", []string{"surfer_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fe", "person surfing: Medium-Dark Skin Tone", []string{"surfer_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3ff", "person surfing: Dark Skin Tone", []string{"surfer_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3c4\U0001f3fb\u200d\u2642\ufe0f", "man surfing: Light Skin Tone", []string{"surfing_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fc\u200d\u2642\ufe0f", "man surfing: Medium-Light Skin Tone", []string{"surfing_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fd\u200d\u2642\ufe0f", "man surfing: Medium Skin Tone", []string{"surfing_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fe\u200d\u2642\ufe0f", "man surfing: Medium-Dark Skin Tone", []string{"surfing_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3ff\u200d\u2642\ufe0f", "man surfing: Dark Skin Tone", []string{"surfing_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3c4\U0001f3fb\u200d\u2642\ufe0f", "man surfing: Light Skin Tone", []string{"surfing_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c4\U0001f3ff\u200d\u2640\ufe0f", "woman surfing: Dark Skin Tone", []string{"surfing_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fb\u200d\u2640\ufe0f", "woman surfing: Light Skin Tone", []string{"surfing_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fc\u200d\u2640\ufe0f", "woman surfing: Medium-Light Skin Tone", []string{"surfing_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fd\u200d\u2640\ufe0f", "woman surfing: Medium Skin Tone", []string{"surfing_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3c4\U0001f3fe\u200d\u2640\ufe0f", "woman surfing: Medium-Dark Skin Tone", []string{"surfing_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3c4\U0001f3ff\u200d\u2640\ufe0f", "woman surfing: Dark Skin Tone", []string{"surfing_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3ca\U0001f3fb", "person swimming: Light Skin Tone", []string{"swimmer_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f3ca\U0001f3fc", "person swimming: Medium-Light Skin Tone", []string{"swimmer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fd", "person swimming: Medium Skin Tone", []string{"swimmer_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fe", "person swimming: Medium-Dark Skin Tone", []string{"swimmer_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3ff", "person swimming: Dark Skin Tone", []string{"swimmer_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3ca\U0001f3fb", "person swimming: Light Skin Tone", []string{"swimmer_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3ca\U0001f3fc", "person swimming: Medium-Light Skin Tone", []string{"swimmer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fb\u200d\u2642\ufe0f", "man swimming: Light Skin Tone", []string{"swimming_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fc\u200d\u2642\ufe0f", "man swimming: Medium-Light Skin Tone", []string{"swimming_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fd\u200d\u2642\ufe0f", "man swimming: Medium Skin Tone", []string{"swimming_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fe\u200d\u2642\ufe0f", "man swimming: Medium-Dark Skin Tone", []string{"swimming_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3ff\u200d\u2642\ufe0f", "man swimming: Dark Skin Tone", []string{"swimming_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3ca\U0001f3fe\u200d\u2640\ufe0f", "woman swimming: Medium-Dark Skin Tone", []string{"swimming_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3ca\U0001f3ff\u200d\u2640\ufe0f", "woman swimming: Dark Skin Tone", []string{"swimming_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fb\u200d\u2640\ufe0f", "woman swimming: Light Skin Tone", []string{"swimming_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fc\u200d\u2640\ufe0f", "woman swimming: Medium-Light Skin Tone", []string{"swimming_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3ca\U0001f3fd\u200d\u2640\ufe0f", "woman swimming: Medium Skin Tone", []string{"swimming_woman_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f3ca\U0001f3fe\u200d\u2640\ufe0f", "woman swimming: Medium-Dark Skin Tone", []string{"swimming_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3ca\U0001f3ff\u200d\u2640\ufe0f", "woman swimming: Dark Skin Tone", []string{"swimming_woman_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f3eb", "teacher: Medium-Dark Skin Tone", []string{"teacher_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f3eb", "teacher: Dark Skin Tone", []string{"teacher_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f3eb", "teacher: Light Skin Tone", []string{"teacher_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f3eb", "teacher: Medium-Light Skin Tone", []string{"teacher_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f3eb", "teacher: Medium Skin Tone", []string{"teacher_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fe\u200d\U0001f3eb", "teacher: Medium-Dark Skin Tone", []string{"teacher_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f3eb", "teacher: Dark Skin Tone", []string{"teacher_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f4bb", "technologist: Light Skin Tone", []string{"technologist_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f4bb", "technologist: Medium-Light Skin Tone", []string{"technologist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f4bb", "technologist: Medium Skin Tone", []string{"technologist_Medium_Skin_Tone"}, "12.0", false}, @@ -2905,31 +3106,31 @@ var GemojiData = Gemoji{ {"\U0001f481\U0001f3fd", "person tipping hand: Medium Skin Tone", []string{"tipping_hand_person_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f481\U0001f3fe", "person tipping hand: Medium-Dark Skin Tone", []string{"tipping_hand_person_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f481\U0001f3ff", "person tipping hand: Dark Skin Tone", []string{"tipping_hand_person_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f481\U0001f3fb\u200d\u2640\ufe0f", "woman tipping hand: Light Skin Tone", []string{"tipping_hand_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f481\U0001f3fc\u200d\u2640\ufe0f", "woman tipping hand: Medium-Light Skin Tone", []string{"tipping_hand_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f481\U0001f3fd\u200d\u2640\ufe0f", "woman tipping hand: Medium Skin Tone", []string{"tipping_hand_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f481\U0001f3fe\u200d\u2640\ufe0f", "woman tipping hand: Medium-Dark Skin Tone", []string{"tipping_hand_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f481\U0001f3ff\u200d\u2640\ufe0f", "woman tipping hand: Dark Skin Tone", []string{"tipping_hand_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f46c\U0001f3fb", "men holding hands: Light Skin Tone", []string{"two_men_holding_hands_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f481\U0001f3fb\u200d\u2640\ufe0f", "woman tipping hand: Light Skin Tone", []string{"tipping_hand_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f46c\U0001f3fc", "men holding hands: Medium-Light Skin Tone", []string{"two_men_holding_hands_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f46c\U0001f3fd", "men holding hands: Medium Skin Tone", []string{"two_men_holding_hands_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f46c\U0001f3fe", "men holding hands: Medium-Dark Skin Tone", []string{"two_men_holding_hands_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46c\U0001f3ff", "men holding hands: Dark Skin Tone", []string{"two_men_holding_hands_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f46c\U0001f3fb", "men holding hands: Light Skin Tone", []string{"two_men_holding_hands_Light_Skin_Tone"}, "12.0", false}, {"\U0001f46d\U0001f3fb", "women holding hands: Light Skin Tone", []string{"two_women_holding_hands_Light_Skin_Tone"}, "12.0", false}, {"\U0001f46d\U0001f3fc", "women holding hands: Medium-Light Skin Tone", []string{"two_women_holding_hands_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f46d\U0001f3fd", "women holding hands: Medium Skin Tone", []string{"two_women_holding_hands_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f46d\U0001f3fe", "women holding hands: Medium-Dark Skin Tone", []string{"two_women_holding_hands_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f46d\U0001f3ff", "women holding hands: Dark Skin Tone", []string{"two_women_holding_hands_Dark_Skin_Tone"}, "12.0", false}, + {"\u270c\U0001f3fe\ufe0f", "victory hand: Medium-Dark Skin Tone", []string{"v_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\u270c\U0001f3ff\ufe0f", "victory hand: Dark Skin Tone", []string{"v_Dark_Skin_Tone"}, "12.0", false}, {"\u270c\U0001f3fb\ufe0f", "victory hand: Light Skin Tone", []string{"v_Light_Skin_Tone"}, "12.0", false}, {"\u270c\U0001f3fc\ufe0f", "victory hand: Medium-Light Skin Tone", []string{"v_Medium-Light_Skin_Tone"}, "12.0", false}, {"\u270c\U0001f3fd\ufe0f", "victory hand: Medium Skin Tone", []string{"v_Medium_Skin_Tone"}, "12.0", false}, - {"\u270c\U0001f3fe\ufe0f", "victory hand: Medium-Dark Skin Tone", []string{"v_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\u270c\U0001f3ff\ufe0f", "victory hand: Dark Skin Tone", []string{"v_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9db\U0001f3fd", "vampire: Medium Skin Tone", []string{"vampire_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9db\U0001f3fe", "vampire: Medium-Dark Skin Tone", []string{"vampire_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9db\U0001f3ff", "vampire: Dark Skin Tone", []string{"vampire_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9db\U0001f3fb", "vampire: Light Skin Tone", []string{"vampire_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9db\U0001f3fc", "vampire: Medium-Light Skin Tone", []string{"vampire_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9db\U0001f3fd", "vampire: Medium Skin Tone", []string{"vampire_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9db\U0001f3fb\u200d\u2642\ufe0f", "man vampire: Light Skin Tone", []string{"vampire_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9db\U0001f3fc\u200d\u2642\ufe0f", "man vampire: Medium-Light Skin Tone", []string{"vampire_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9db\U0001f3fd\u200d\u2642\ufe0f", "man vampire: Medium Skin Tone", []string{"vampire_man_Medium_Skin_Tone"}, "12.0", false}, @@ -2950,66 +3151,71 @@ var GemojiData = Gemoji{ {"\U0001f6b6\U0001f3fd", "person walking: Medium Skin Tone", []string{"walking_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3fe", "person walking: Medium-Dark Skin Tone", []string{"walking_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3ff", "person walking: Dark Skin Tone", []string{"walking_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f", "man walking: Light Skin Tone", []string{"walking_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f", "man walking: Medium-Light Skin Tone", []string{"walking_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f", "man walking: Medium Skin Tone", []string{"walking_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f", "man walking: Medium-Dark Skin Tone", []string{"walking_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f", "man walking: Dark Skin Tone", []string{"walking_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f", "man walking: Light Skin Tone", []string{"walking_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f", "woman walking: Medium-Light Skin Tone", []string{"walking_woman_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f", "woman walking: Medium Skin Tone", []string{"walking_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f", "woman walking: Medium-Dark Skin Tone", []string{"walking_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f", "woman walking: Dark Skin Tone", []string{"walking_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f", "woman walking: Light Skin Tone", []string{"walking_woman_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f", "woman walking: Medium-Light Skin Tone", []string{"walking_woman_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f", "woman walking: Medium Skin Tone", []string{"walking_woman_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f93d\U0001f3fb", "person playing water polo: Light Skin Tone", []string{"water_polo_Light_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fc", "person playing water polo: Medium-Light Skin Tone", []string{"water_polo_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fd", "person playing water polo: Medium Skin Tone", []string{"water_polo_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fe", "person playing water polo: Medium-Dark Skin Tone", []string{"water_polo_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3ff", "person playing water polo: Dark Skin Tone", []string{"water_polo_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f44b\U0001f3ff", "waving hand: Dark Skin Tone", []string{"wave_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f44b\U0001f3fb", "waving hand: Light Skin Tone", []string{"wave_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f93d\U0001f3fb", "person playing water polo: Light Skin Tone", []string{"water_polo_Light_Skin_Tone"}, "12.0", false}, {"\U0001f44b\U0001f3fc", "waving hand: Medium-Light Skin Tone", []string{"wave_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f44b\U0001f3fd", "waving hand: Medium Skin Tone", []string{"wave_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f44b\U0001f3fe", "waving hand: Medium-Dark Skin Tone", []string{"wave_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3cb\U0001f3fb\ufe0f", "person lifting weights: Light Skin Tone", []string{"weight_lifting_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f3cb\U0001f3fc\ufe0f", "person lifting weights: Medium-Light Skin Tone", []string{"weight_lifting_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f44b\U0001f3ff", "waving hand: Dark Skin Tone", []string{"wave_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f44b\U0001f3fb", "waving hand: Light Skin Tone", []string{"wave_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fd\ufe0f", "person lifting weights: Medium Skin Tone", []string{"weight_lifting_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fe\ufe0f", "person lifting weights: Medium-Dark Skin Tone", []string{"weight_lifting_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3ff\ufe0f", "person lifting weights: Dark Skin Tone", []string{"weight_lifting_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3cb\U0001f3fb\ufe0f", "person lifting weights: Light Skin Tone", []string{"weight_lifting_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3cb\U0001f3fc\ufe0f", "person lifting weights: Medium-Light Skin Tone", []string{"weight_lifting_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fb\ufe0f\u200d\u2642\ufe0f", "man lifting weights: Light Skin Tone", []string{"weight_lifting_man_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fc\ufe0f\u200d\u2642\ufe0f", "man lifting weights: Medium-Light Skin Tone", []string{"weight_lifting_man_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fd\ufe0f\u200d\u2642\ufe0f", "man lifting weights: Medium Skin Tone", []string{"weight_lifting_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fe\ufe0f\u200d\u2642\ufe0f", "man lifting weights: Medium-Dark Skin Tone", []string{"weight_lifting_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3ff\ufe0f\u200d\u2642\ufe0f", "man lifting weights: Dark Skin Tone", []string{"weight_lifting_man_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f3cb\U0001f3fb\ufe0f\u200d\u2640\ufe0f", "woman lifting weights: Light Skin Tone", []string{"weight_lifting_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fc\ufe0f\u200d\u2640\ufe0f", "woman lifting weights: Medium-Light Skin Tone", []string{"weight_lifting_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fd\ufe0f\u200d\u2640\ufe0f", "woman lifting weights: Medium Skin Tone", []string{"weight_lifting_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3fe\ufe0f\u200d\u2640\ufe0f", "woman lifting weights: Medium-Dark Skin Tone", []string{"weight_lifting_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f3cb\U0001f3ff\ufe0f\u200d\u2640\ufe0f", "woman lifting weights: Dark Skin Tone", []string{"weight_lifting_woman_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fc\u200d\U0001f9b3", "man: white hair: Medium-Light Skin Tone", []string{"white_haired_man_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f468\U0001f3fd\u200d\U0001f9b3", "man: white hair: Medium Skin Tone", []string{"white_haired_man_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f3cb\U0001f3fb\ufe0f\u200d\u2640\ufe0f", "woman lifting weights: Light Skin Tone", []string{"weight_lifting_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f9b3", "man: white hair: Medium-Dark Skin Tone", []string{"white_haired_man_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3ff\u200d\U0001f9b3", "man: white hair: Dark Skin Tone", []string{"white_haired_man_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f9b3", "man: white hair: Light Skin Tone", []string{"white_haired_man_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f9b3", "man: white hair: Medium-Light Skin Tone", []string{"white_haired_man_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fd\u200d\U0001f9b3", "man: white hair: Medium Skin Tone", []string{"white_haired_man_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f9b3", "woman: white hair: Dark Skin Tone", []string{"white_haired_woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f9b3", "woman: white hair: Light Skin Tone", []string{"white_haired_woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9b3", "woman: white hair: Medium-Light Skin Tone", []string{"white_haired_woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f9b3", "woman: white hair: Medium Skin Tone", []string{"white_haired_woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f9b3", "woman: white hair: Medium-Dark Skin Tone", []string{"white_haired_woman_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff", "woman: Dark Skin Tone", []string{"woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb", "woman: Light Skin Tone", []string{"woman_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc", "woman: Medium-Light Skin Tone", []string{"woman_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd", "woman: Medium Skin Tone", []string{"woman_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe", "woman: Medium-Dark Skin Tone", []string{"woman_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff", "woman: Dark Skin Tone", []string{"woman_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f3a8", "woman artist: Light Skin Tone", []string{"woman_artist_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f3a8", "woman artist: Medium-Light Skin Tone", []string{"woman_artist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f3a8", "woman artist: Medium Skin Tone", []string{"woman_artist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f3a8", "woman artist: Medium-Dark Skin Tone", []string{"woman_artist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f3a8", "woman artist: Dark Skin Tone", []string{"woman_artist_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f680", "woman astronaut: Dark Skin Tone", []string{"woman_astronaut_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f680", "woman astronaut: Light Skin Tone", []string{"woman_astronaut_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f680", "woman astronaut: Medium-Light Skin Tone", []string{"woman_astronaut_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f680", "woman astronaut: Medium Skin Tone", []string{"woman_astronaut_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f680", "woman astronaut: Medium-Dark Skin Tone", []string{"woman_astronaut_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f680", "woman astronaut: Dark Skin Tone", []string{"woman_astronaut_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fd\u200d\u2640\ufe0f", "woman: beard: Medium Skin Tone", []string{"woman_beard_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fe\u200d\u2640\ufe0f", "woman: beard: Medium-Dark Skin Tone", []string{"woman_beard_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3ff\u200d\u2640\ufe0f", "woman: beard: Dark Skin Tone", []string{"woman_beard_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fb\u200d\u2640\ufe0f", "woman: beard: Light Skin Tone", []string{"woman_beard_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d4\U0001f3fc\u200d\u2640\ufe0f", "woman: beard: Medium-Light Skin Tone", []string{"woman_beard_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fb\u200d\u2640\ufe0f", "woman cartwheeling: Light Skin Tone", []string{"woman_cartwheeling_Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fc\u200d\u2640\ufe0f", "woman cartwheeling: Medium-Light Skin Tone", []string{"woman_cartwheeling_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f938\U0001f3fd\u200d\u2640\ufe0f", "woman cartwheeling: Medium Skin Tone", []string{"woman_cartwheeling_Medium_Skin_Tone"}, "12.0", false}, @@ -3025,46 +3231,56 @@ var GemojiData = Gemoji{ {"\U0001f483\U0001f3fd", "woman dancing: Medium Skin Tone", []string{"woman_dancing_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f483\U0001f3fe", "woman dancing: Medium-Dark Skin Tone", []string{"woman_dancing_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f483\U0001f3ff", "woman dancing: Dark Skin Tone", []string{"woman_dancing_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f926\U0001f3ff\u200d\u2640\ufe0f", "woman facepalming: Dark Skin Tone", []string{"woman_facepalming_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fb\u200d\u2640\ufe0f", "woman facepalming: Light Skin Tone", []string{"woman_facepalming_Light_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fc\u200d\u2640\ufe0f", "woman facepalming: Medium-Light Skin Tone", []string{"woman_facepalming_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fd\u200d\u2640\ufe0f", "woman facepalming: Medium Skin Tone", []string{"woman_facepalming_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f926\U0001f3fe\u200d\u2640\ufe0f", "woman facepalming: Medium-Dark Skin Tone", []string{"woman_facepalming_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f926\U0001f3ff\u200d\u2640\ufe0f", "woman facepalming: Dark Skin Tone", []string{"woman_facepalming_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f3ed", "woman factory worker: Light Skin Tone", []string{"woman_factory_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f3ed", "woman factory worker: Medium-Light Skin Tone", []string{"woman_factory_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f3ed", "woman factory worker: Medium Skin Tone", []string{"woman_factory_worker_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f3ed", "woman factory worker: Medium-Dark Skin Tone", []string{"woman_factory_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f3ed", "woman factory worker: Dark Skin Tone", []string{"woman_factory_worker_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f33e", "woman farmer: Medium-Dark Skin Tone", []string{"woman_farmer_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f33e", "woman farmer: Dark Skin Tone", []string{"woman_farmer_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f33e", "woman farmer: Light Skin Tone", []string{"woman_farmer_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f33e", "woman farmer: Medium-Light Skin Tone", []string{"woman_farmer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f33e", "woman farmer: Medium Skin Tone", []string{"woman_farmer_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fe\u200d\U0001f33e", "woman farmer: Medium-Dark Skin Tone", []string{"woman_farmer_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f33e", "woman farmer: Dark Skin Tone", []string{"woman_farmer_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\U0001f37c", "woman feeding baby: Medium-Light Skin Tone", []string{"woman_feeding_baby_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fd\u200d\U0001f37c", "woman feeding baby: Medium Skin Tone", []string{"woman_feeding_baby_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f37c", "woman feeding baby: Medium-Dark Skin Tone", []string{"woman_feeding_baby_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f37c", "woman feeding baby: Dark Skin Tone", []string{"woman_feeding_baby_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\U0001f37c", "woman feeding baby: Light Skin Tone", []string{"woman_feeding_baby_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f692", "woman firefighter: Medium-Light Skin Tone", []string{"woman_firefighter_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f692", "woman firefighter: Medium Skin Tone", []string{"woman_firefighter_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f692", "woman firefighter: Medium-Dark Skin Tone", []string{"woman_firefighter_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f692", "woman firefighter: Dark Skin Tone", []string{"woman_firefighter_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f692", "woman firefighter: Light Skin Tone", []string{"woman_firefighter_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\u2695\ufe0f", "woman health worker: Dark Skin Tone", []string{"woman_health_worker_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\u2695\ufe0f", "woman health worker: Light Skin Tone", []string{"woman_health_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\u2695\ufe0f", "woman health worker: Medium-Light Skin Tone", []string{"woman_health_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\u2695\ufe0f", "woman health worker: Medium Skin Tone", []string{"woman_health_worker_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\u2695\ufe0f", "woman health worker: Medium-Dark Skin Tone", []string{"woman_health_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\u2695\ufe0f", "woman health worker: Dark Skin Tone", []string{"woman_health_worker_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fe\u200d\U0001f9bd", "woman in manual wheelchair: Medium-Dark Skin Tone", []string{"woman_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f9bd", "woman in manual wheelchair: Dark Skin Tone", []string{"woman_in_manual_wheelchair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f9bd", "woman in manual wheelchair: Light Skin Tone", []string{"woman_in_manual_wheelchair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9bd", "woman in manual wheelchair: Medium-Light Skin Tone", []string{"woman_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f9bd", "woman in manual wheelchair: Medium Skin Tone", []string{"woman_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f9bd", "woman in manual wheelchair: Medium-Dark Skin Tone", []string{"woman_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f9bd", "woman in manual wheelchair: Dark Skin Tone", []string{"woman_in_manual_wheelchair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f9bc", "woman in motorized wheelchair: Light Skin Tone", []string{"woman_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9bc", "woman in motorized wheelchair: Medium-Light Skin Tone", []string{"woman_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f9bc", "woman in motorized wheelchair: Medium Skin Tone", []string{"woman_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f9bc", "woman in motorized wheelchair: Medium-Dark Skin Tone", []string{"woman_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f9bc", "woman in motorized wheelchair: Dark Skin Tone", []string{"woman_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3ff\u200d\u2640\ufe0f", "woman in tuxedo: Dark Skin Tone", []string{"woman_in_tuxedo_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fb\u200d\u2640\ufe0f", "woman in tuxedo: Light Skin Tone", []string{"woman_in_tuxedo_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fc\u200d\u2640\ufe0f", "woman in tuxedo: Medium-Light Skin Tone", []string{"woman_in_tuxedo_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fd\u200d\u2640\ufe0f", "woman in tuxedo: Medium Skin Tone", []string{"woman_in_tuxedo_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f935\U0001f3fe\u200d\u2640\ufe0f", "woman in tuxedo: Medium-Dark Skin Tone", []string{"woman_in_tuxedo_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\u2696\ufe0f", "woman judge: Light Skin Tone", []string{"woman_judge_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\u2696\ufe0f", "woman judge: Medium-Light Skin Tone", []string{"woman_judge_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\u2696\ufe0f", "woman judge: Medium Skin Tone", []string{"woman_judge_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\u2696\ufe0f", "woman judge: Medium-Dark Skin Tone", []string{"woman_judge_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\u2696\ufe0f", "woman judge: Dark Skin Tone", []string{"woman_judge_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\u2696\ufe0f", "woman judge: Light Skin Tone", []string{"woman_judge_Light_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fb\u200d\u2640\ufe0f", "woman juggling: Light Skin Tone", []string{"woman_juggling_Light_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fc\u200d\u2640\ufe0f", "woman juggling: Medium-Light Skin Tone", []string{"woman_juggling_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fd\u200d\u2640\ufe0f", "woman juggling: Medium Skin Tone", []string{"woman_juggling_Medium_Skin_Tone"}, "12.0", false}, @@ -3075,16 +3291,16 @@ var GemojiData = Gemoji{ {"\U0001f469\U0001f3fd\u200d\U0001f527", "woman mechanic: Medium Skin Tone", []string{"woman_mechanic_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f527", "woman mechanic: Medium-Dark Skin Tone", []string{"woman_mechanic_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f527", "woman mechanic: Dark Skin Tone", []string{"woman_mechanic_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f4bc", "woman office worker: Medium-Dark Skin Tone", []string{"woman_office_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f4bc", "woman office worker: Dark Skin Tone", []string{"woman_office_worker_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f4bc", "woman office worker: Light Skin Tone", []string{"woman_office_worker_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f4bc", "woman office worker: Medium-Light Skin Tone", []string{"woman_office_worker_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f4bc", "woman office worker: Medium Skin Tone", []string{"woman_office_worker_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fe\u200d\U0001f4bc", "woman office worker: Medium-Dark Skin Tone", []string{"woman_office_worker_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f4bc", "woman office worker: Dark Skin Tone", []string{"woman_office_worker_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\u2708\ufe0f", "woman pilot: Light Skin Tone", []string{"woman_pilot_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\u2708\ufe0f", "woman pilot: Medium-Light Skin Tone", []string{"woman_pilot_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\u2708\ufe0f", "woman pilot: Medium Skin Tone", []string{"woman_pilot_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\u2708\ufe0f", "woman pilot: Medium-Dark Skin Tone", []string{"woman_pilot_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\u2708\ufe0f", "woman pilot: Dark Skin Tone", []string{"woman_pilot_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\u2708\ufe0f", "woman pilot: Light Skin Tone", []string{"woman_pilot_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fc\u200d\u2708\ufe0f", "woman pilot: Medium-Light Skin Tone", []string{"woman_pilot_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fb\u200d\u2640\ufe0f", "woman playing handball: Light Skin Tone", []string{"woman_playing_handball_Light_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fc\u200d\u2640\ufe0f", "woman playing handball: Medium-Light Skin Tone", []string{"woman_playing_handball_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93e\U0001f3fd\u200d\u2640\ufe0f", "woman playing handball: Medium Skin Tone", []string{"woman_playing_handball_Medium_Skin_Tone"}, "12.0", false}, @@ -3095,51 +3311,56 @@ var GemojiData = Gemoji{ {"\U0001f93d\U0001f3fd\u200d\u2640\ufe0f", "woman playing water polo: Medium Skin Tone", []string{"woman_playing_water_polo_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fe\u200d\u2640\ufe0f", "woman playing water polo: Medium-Dark Skin Tone", []string{"woman_playing_water_polo_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3ff\u200d\u2640\ufe0f", "woman playing water polo: Dark Skin Tone", []string{"woman_playing_water_polo_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\U0001f52c", "woman scientist: Light Skin Tone", []string{"woman_scientist_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fc\u200d\U0001f52c", "woman scientist: Medium-Light Skin Tone", []string{"woman_scientist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f52c", "woman scientist: Medium Skin Tone", []string{"woman_scientist_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f52c", "woman scientist: Medium-Dark Skin Tone", []string{"woman_scientist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f52c", "woman scientist: Dark Skin Tone", []string{"woman_scientist_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\U0001f52c", "woman scientist: Light Skin Tone", []string{"woman_scientist_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\U0001f52c", "woman scientist: Medium-Light Skin Tone", []string{"woman_scientist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3ff\u200d\u2640\ufe0f", "woman shrugging: Dark Skin Tone", []string{"woman_shrugging_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fb\u200d\u2640\ufe0f", "woman shrugging: Light Skin Tone", []string{"woman_shrugging_Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fc\u200d\u2640\ufe0f", "woman shrugging: Medium-Light Skin Tone", []string{"woman_shrugging_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fd\u200d\u2640\ufe0f", "woman shrugging: Medium Skin Tone", []string{"woman_shrugging_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f937\U0001f3fe\u200d\u2640\ufe0f", "woman shrugging: Medium-Dark Skin Tone", []string{"woman_shrugging_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f3a4", "woman singer: Medium-Dark Skin Tone", []string{"woman_singer_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f3a4", "woman singer: Dark Skin Tone", []string{"woman_singer_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f3a4", "woman singer: Light Skin Tone", []string{"woman_singer_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f3a4", "woman singer: Medium-Light Skin Tone", []string{"woman_singer_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f3a4", "woman singer: Medium Skin Tone", []string{"woman_singer_Medium_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fe\u200d\U0001f3a4", "woman singer: Medium-Dark Skin Tone", []string{"woman_singer_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f3a4", "woman singer: Dark Skin Tone", []string{"woman_singer_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f393", "woman student: Light Skin Tone", []string{"woman_student_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f393", "woman student: Medium-Light Skin Tone", []string{"woman_student_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f393", "woman student: Medium Skin Tone", []string{"woman_student_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f393", "woman student: Medium-Dark Skin Tone", []string{"woman_student_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f393", "woman student: Dark Skin Tone", []string{"woman_student_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\U0001f3eb", "woman teacher: Light Skin Tone", []string{"woman_teacher_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f3eb", "woman teacher: Medium-Light Skin Tone", []string{"woman_teacher_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f3eb", "woman teacher: Medium Skin Tone", []string{"woman_teacher_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f3eb", "woman teacher: Medium-Dark Skin Tone", []string{"woman_teacher_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3ff\u200d\U0001f3eb", "woman teacher: Dark Skin Tone", []string{"woman_teacher_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fb\u200d\U0001f3eb", "woman teacher: Light Skin Tone", []string{"woman_teacher_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fe\u200d\U0001f4bb", "woman technologist: Medium-Dark Skin Tone", []string{"woman_technologist_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f4bb", "woman technologist: Dark Skin Tone", []string{"woman_technologist_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f4bb", "woman technologist: Light Skin Tone", []string{"woman_technologist_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f4bb", "woman technologist: Medium-Light Skin Tone", []string{"woman_technologist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f4bb", "woman technologist: Medium Skin Tone", []string{"woman_technologist_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f4bb", "woman technologist: Medium-Dark Skin Tone", []string{"woman_technologist_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f4bb", "woman technologist: Dark Skin Tone", []string{"woman_technologist_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d5\U0001f3fb", "woman with headscarf: Light Skin Tone", []string{"woman_with_headscarf_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d5\U0001f3fc", "woman with headscarf: Medium-Light Skin Tone", []string{"woman_with_headscarf_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d5\U0001f3fd", "woman with headscarf: Medium Skin Tone", []string{"woman_with_headscarf_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d5\U0001f3fe", "woman with headscarf: Medium-Dark Skin Tone", []string{"woman_with_headscarf_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d5\U0001f3ff", "woman with headscarf: Dark Skin Tone", []string{"woman_with_headscarf_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d5\U0001f3fb", "woman with headscarf: Light Skin Tone", []string{"woman_with_headscarf_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d5\U0001f3fc", "woman with headscarf: Medium-Light Skin Tone", []string{"woman_with_headscarf_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3fe\u200d\U0001f9af", "woman with white cane: Medium-Dark Skin Tone", []string{"woman_with_probing_cane_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f469\U0001f3ff\u200d\U0001f9af", "woman with white cane: Dark Skin Tone", []string{"woman_with_probing_cane_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f9af", "woman with white cane: Light Skin Tone", []string{"woman_with_probing_cane_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9af", "woman with white cane: Medium-Light Skin Tone", []string{"woman_with_probing_cane_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f9af", "woman with white cane: Medium Skin Tone", []string{"woman_with_probing_cane_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f9af", "woman with white cane: Medium-Dark Skin Tone", []string{"woman_with_probing_cane_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3ff\u200d\U0001f9af", "woman with white cane: Dark Skin Tone", []string{"woman_with_probing_cane_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fd\u200d\u2640\ufe0f", "woman wearing turban: Medium Skin Tone", []string{"woman_with_turban_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fe\u200d\u2640\ufe0f", "woman wearing turban: Medium-Dark Skin Tone", []string{"woman_with_turban_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3ff\u200d\u2640\ufe0f", "woman wearing turban: Dark Skin Tone", []string{"woman_with_turban_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fb\u200d\u2640\ufe0f", "woman wearing turban: Light Skin Tone", []string{"woman_with_turban_Light_Skin_Tone"}, "12.0", false}, {"\U0001f473\U0001f3fc\u200d\u2640\ufe0f", "woman wearing turban: Medium-Light Skin Tone", []string{"woman_with_turban_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3ff\u200d\u2640\ufe0f", "woman with veil: Dark Skin Tone", []string{"woman_with_veil_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fb\u200d\u2640\ufe0f", "woman with veil: Light Skin Tone", []string{"woman_with_veil_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fc\u200d\u2640\ufe0f", "woman with veil: Medium-Light Skin Tone", []string{"woman_with_veil_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fd\u200d\u2640\ufe0f", "woman with veil: Medium Skin Tone", []string{"woman_with_veil_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f470\U0001f3fe\u200d\u2640\ufe0f", "woman with veil: Medium-Dark Skin Tone", []string{"woman_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\u270d\U0001f3fb\ufe0f", "writing hand: Light Skin Tone", []string{"writing_hand_Light_Skin_Tone"}, "12.0", false}, {"\u270d\U0001f3fc\ufe0f", "writing hand: Medium-Light Skin Tone", []string{"writing_hand_Medium-Light_Skin_Tone"}, "12.0", false}, {"\u270d\U0001f3fd\ufe0f", "writing hand: Medium Skin Tone", []string{"writing_hand_Medium_Skin_Tone"}, "12.0", false}, diff --git a/modules/git/blame.go b/modules/git/blame.go index 832b12213c..535710c4cd 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -24,12 +24,12 @@ type BlamePart struct { // BlameReader returns part of file blame one by one type BlameReader struct { - cmd *exec.Cmd - output io.ReadCloser - reader *bufio.Reader - lastSha *string - cancel context.CancelFunc // Cancels the context that this reader runs in - finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table + cmd *exec.Cmd + reader io.ReadCloser + lastSha *string + cancel context.CancelFunc // Cancels the context that this reader runs in + finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table + bufferedReader *bufio.Reader } var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") @@ -38,8 +38,6 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})") func (r *BlameReader) NextPart() (*BlamePart, error) { var blamePart *BlamePart - reader := r.reader - if r.lastSha != nil { blamePart = &BlamePart{*r.lastSha, make([]string, 0)} } @@ -49,7 +47,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { var err error for err != io.EOF { - line, isPrefix, err = reader.ReadLine() + line, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -71,7 +69,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { r.lastSha = &sha1 // need to munch to end of line... for isPrefix { - _, isPrefix, err = reader.ReadLine() + _, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -86,7 +84,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // need to munch to end of line... for isPrefix { - _, isPrefix, err = reader.ReadLine() + _, isPrefix, err = r.bufferedReader.ReadLine() if err != nil && err != io.EOF { return blamePart, err } @@ -102,9 +100,9 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { func (r *BlameReader) Close() error { defer r.finished() // Only remove the process from the process table when the underlying command is closed r.cancel() // However, first cancel our own context early + r.bufferedReader = nil - _ = r.output.Close() - + _ = r.reader.Close() if err := r.cmd.Wait(); err != nil { return fmt.Errorf("Wait: %w", err) } @@ -126,25 +124,27 @@ func createBlameReader(ctx context.Context, dir string, command ...string) (*Bla cmd.Stderr = os.Stderr process.SetSysProcAttribute(cmd) - stdout, err := cmd.StdoutPipe() + reader, stdout, err := os.Pipe() if err != nil { defer finished() return nil, fmt.Errorf("StdoutPipe: %w", err) } + cmd.Stdout = stdout if err = cmd.Start(); err != nil { defer finished() _ = stdout.Close() return nil, fmt.Errorf("Start: %w", err) } + _ = stdout.Close() - reader := bufio.NewReader(stdout) + bufferedReader := bufio.NewReader(reader) return &BlameReader{ - cmd: cmd, - output: stdout, - reader: reader, - cancel: cancel, - finished: finished, + cmd: cmd, + reader: reader, + cancel: cancel, + finished: finished, + bufferedReader: bufferedReader, }, nil } diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go index 4bee8cd27a..a37b7a45ea 100644 --- a/modules/git/blame_test.go +++ b/modules/git/blame_test.go @@ -65,7 +65,7 @@ summary Add code of delete user previous be0ba9ea88aff8a658d0495d36accf944b74888d gogs.go filename gogs.go // license that can be found in the LICENSE file. - + ` + ` e2aa991e10ffd924a828ec149951f2f20eecead2 6 6 2 author Lunny Xiao author-mail @@ -112,9 +112,7 @@ func TestReadingBlameOutput(t *testing.T) { }, { "ce21ed6c3490cdfad797319cbb1145e2330a8fef", - []string{ - "// Copyright 2016 The Gitea Authors. All rights reserved.", - }, + []string{"// Copyright 2016 The Gitea Authors. All rights reserved."}, }, { "4b92a6c2df28054ad766bc262f308db9f6066596", diff --git a/modules/git/command.go b/modules/git/command.go index abf40b0cd7..0d94494f11 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -202,8 +202,11 @@ func (c *Command) Run(opts *RunOpts) error { if opts == nil { opts = &RunOpts{} } - if opts.Timeout <= 0 { - opts.Timeout = defaultCommandExecutionTimeout + + // We must not change the provided options + timeout := opts.Timeout + if timeout <= 0 { + timeout = defaultCommandExecutionTimeout } if len(opts.Dir) == 0 { @@ -238,7 +241,7 @@ func (c *Command) Run(opts *RunOpts) error { if opts.UseContextTimeout { ctx, cancel, finished = process.GetManager().AddContext(c.parentContext, desc) } else { - ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc) + ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, timeout, desc) } defer finished() @@ -339,9 +342,20 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS } stdoutBuf := &bytes.Buffer{} stderrBuf := &bytes.Buffer{} - opts.Stdout = stdoutBuf - opts.Stderr = stderrBuf - err := c.Run(opts) + + // We must not change the provided options as it could break future calls - therefore make a copy. + newOpts := &RunOpts{ + Env: opts.Env, + Timeout: opts.Timeout, + UseContextTimeout: opts.UseContextTimeout, + Dir: opts.Dir, + Stdout: stdoutBuf, + Stderr: stderrBuf, + Stdin: opts.Stdin, + PipelineFunc: opts.PipelineFunc, + } + + err := c.Run(newOpts) stderr = stderrBuf.Bytes() if err != nil { return nil, stderr, &runStdError{err: err, stderr: bytesToString(stderr)} diff --git a/modules/git/commit.go b/modules/git/commit.go index 061adc1082..ec2cc8ad81 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -132,7 +132,7 @@ func CommitChangesWithArgs(repoPath string, args []CmdArg, opts CommitChangesOpt if opts.Author != nil { cmd.AddArguments(CmdArg(fmt.Sprintf("--author='%s <%s>'", opts.Author.Name, opts.Author.Email))) } - cmd.AddArguments("-m").AddDynamicArguments(opts.Message) + cmd.AddArguments(CmdArg("--message=" + opts.Message)) _, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) // No stderr but exit status 1 means nothing to commit. diff --git a/modules/git/repo.go b/modules/git/repo.go index 8ba3ae4fda..9dd67c71cf 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -164,10 +164,8 @@ func CloneWithArgs(ctx context.Context, args []CmdArg, from, to string, opts Clo envs := os.Environ() u, err := url.Parse(from) - if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) { - if proxy.Match(u.Host) { - envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL())) - } + if err == nil { + envs = proxy.EnvWithProxy(u) } stderr := new(bytes.Buffer) diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index 95c3718841..26e20ba116 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -53,7 +53,7 @@ func (repo *Repository) IsReferenceExist(name string) bool { // IsBranchExist returns true if given branch exists in current repository. func (repo *Repository) IsBranchExist(name string) bool { - if name == "" { + if repo == nil || name == "" { return false } diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 14fec3f9c6..7a869b38b6 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -42,7 +42,7 @@ func (repo *Repository) RemoveReference(name string) error { // ConvertToSHA1 returns a Hash object from a potential ID string func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) { - if len(commitID) == 40 { + if len(commitID) == SHAFullLength { sha1, err := NewIDFromString(commitID) if err == nil { return sha1, nil diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index 13a7be778f..2d91ee0955 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -138,7 +138,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id SHA1) (*Co // ConvertToSHA1 returns a Hash object from a potential ID string func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) { - if len(commitID) == 40 && IsValidSHAPattern(commitID) { + if len(commitID) == SHAFullLength && IsValidSHAPattern(commitID) { sha1, err := NewIDFromString(commitID) if err == nil { return sha1, nil diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index 5542883288..3ff761d930 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -17,7 +17,7 @@ import ( // ReadTreeToIndex reads a treeish to the index func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) error { - if len(treeish) != 40 { + if len(treeish) != SHAFullLength { res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(treeish).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return err diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 5d3aace52f..0bb0da21bf 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -16,7 +16,7 @@ import ( // IsTagExist returns true if given tag exists in the repository. func (repo *Repository) IsTagExist(name string) bool { - if name == "" { + if repo == nil || name == "" { return false } diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index e720164936..9676bceebe 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -20,7 +20,7 @@ func (repo *Repository) getTree(id SHA1) (*Tree, error) { // GetTree find the tree object in the repository. func (repo *Repository) GetTree(idStr string) (*Tree, error) { - if len(idStr) != 40 { + if len(idStr) != SHAFullLength { res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go index dc4a5becb9..6dea6cf026 100644 --- a/modules/git/repo_tree_nogogit.go +++ b/modules/git/repo_tree_nogogit.go @@ -67,7 +67,7 @@ func (repo *Repository) getTree(id SHA1) (*Tree, error) { // GetTree find the tree object in the repository. func (repo *Repository) GetTree(idStr string) (*Tree, error) { - if len(idStr) != 40 { + if len(idStr) != SHAFullLength { res, err := repo.GetRefCommitID(idStr) if err != nil { return nil, err diff --git a/modules/git/sha1.go b/modules/git/sha1.go index 15f282c6e4..7c777c5e47 100644 --- a/modules/git/sha1.go +++ b/modules/git/sha1.go @@ -18,6 +18,9 @@ const EmptySHA = "0000000000000000000000000000000000000000" // EmptyTreeSHA is the SHA of an empty tree const EmptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" +// SHAFullLength is the full length of a git SHA +const SHAFullLength = 40 + // SHAPattern can be used to determine if a string is an valid sha var shaPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`) @@ -51,7 +54,7 @@ func MustIDFromString(s string) SHA1 { func NewIDFromString(s string) (SHA1, error) { var id SHA1 s = strings.TrimSpace(s) - if len(s) != 40 { + if len(s) != SHAFullLength { return id, fmt.Errorf("Length must be 40: %s", s) } b, err := hex.DecodeString(s) diff --git a/modules/git/signature_gogit.go b/modules/git/signature_gogit.go index 6f1c98420d..5ab38cd852 100644 --- a/modules/git/signature_gogit.go +++ b/modules/git/signature_gogit.go @@ -10,6 +10,7 @@ package git import ( "bytes" "strconv" + "strings" "time" "github.com/go-git/go-git/v5/plumbing/object" @@ -30,7 +31,9 @@ type Signature = object.Signature func newSignatureFromCommitline(line []byte) (_ *Signature, err error) { sig := new(Signature) emailStart := bytes.IndexByte(line, '<') - sig.Name = string(line[:emailStart-1]) + if emailStart > 0 { // Empty name has already occurred, even if it shouldn't + sig.Name = strings.TrimSpace(string(line[:emailStart-1])) + } emailEnd := bytes.IndexByte(line, '>') sig.Email = string(line[emailStart+1 : emailEnd]) diff --git a/modules/git/signature_nogogit.go b/modules/git/signature_nogogit.go index 07a3b79f1e..3fa5c8da3e 100644 --- a/modules/git/signature_nogogit.go +++ b/modules/git/signature_nogogit.go @@ -11,6 +11,7 @@ import ( "bytes" "fmt" "strconv" + "strings" "time" ) @@ -51,7 +52,9 @@ func newSignatureFromCommitline(line []byte) (sig *Signature, err error) { return } - sig.Name = string(line[:emailStart-1]) + if emailStart > 0 { // Empty name has already occurred, even if it shouldn't + sig.Name = strings.TrimSpace(string(line[:emailStart-1])) + } sig.Email = string(line[emailStart+1 : emailEnd]) hasTime := emailEnd+2 < len(line) diff --git a/modules/git/utils.go b/modules/git/utils.go index d6bf9f4413..a439dabae1 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -100,6 +100,9 @@ func RefURL(repoURL, ref string) string { return repoURL + "/src/branch/" + refName case strings.HasPrefix(ref, TagPrefix): return repoURL + "/src/tag/" + refName + case !IsValidSHAPattern(ref): + // assume they mean a branch + return repoURL + "/src/branch/" + refName default: return repoURL + "/src/commit/" + refName } diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 750233d4a7..882f207ef7 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -31,6 +31,7 @@ func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDire // to remind users they are using non-prod setting. h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) + h.Add("X-Forgejo-Debug", "RUN_MODE="+setting.RunMode) } h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", ")) diff --git a/modules/httpcache/httpcache_test.go b/modules/httpcache/httpcache_test.go index 49e54d147e..e7965d6af8 100644 --- a/modules/httpcache/httpcache_test.go +++ b/modules/httpcache/httpcache_test.go @@ -30,6 +30,9 @@ func countFormalHeaders(h http.Header) (c int) { if strings.HasPrefix(k, "X-Gitea-") { continue } + if strings.HasPrefix(k, "X-Forgejo-") { + continue + } c++ } return c diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 3b33852cb5..0bdf5a1987 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -165,7 +165,7 @@ func validateOptions(field *api.IssueFormField, idx int) error { return position.Errorf("should be a string") } case api.IssueFormFieldTypeCheckboxes: - opt, ok := option.(map[interface{}]interface{}) + opt, ok := option.(map[string]interface{}) if !ok { return position.Errorf("should be a dictionary") } @@ -351,7 +351,7 @@ func (o *valuedOption) Label() string { return label } case api.IssueFormFieldTypeCheckboxes: - if vs, ok := o.data.(map[interface{}]interface{}); ok { + if vs, ok := o.data.(map[string]interface{}); ok { if v, ok := vs["label"].(string); ok { return v } diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go index 883e1e0780..c3863a64a6 100644 --- a/modules/issue/template/template_test.go +++ b/modules/issue/template/template_test.go @@ -6,18 +6,21 @@ package template import ( "net/url" - "reflect" "testing" "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/require" ) func TestValidate(t *testing.T) { tests := []struct { - name string - content string - wantErr string + name string + filename string + content string + want *api.IssueTemplate + wantErr string }{ { name: "miss name", @@ -316,21 +319,9 @@ body: `, wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool", }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpl, err := unmarshal("test.yaml", []byte(tt.content)) - if err != nil { - t.Fatal(err) - } - if err := Validate(tmpl); (err == nil) != (tt.wantErr == "") || err != nil && err.Error() != tt.wantErr { - t.Errorf("Validate() error = %v, wantErr %q", err, tt.wantErr) - } - }) - } - - t.Run("valid", func(t *testing.T) { - content := ` + { + name: "valid", + content: ` name: Name title: Title about: About @@ -386,96 +377,227 @@ body: required: false - label: Option 3 of checkboxes required: true -` - want := &api.IssueTemplate{ - Name: "Name", - Title: "Title", - About: "About", - Labels: []string{"label1", "label2"}, - Ref: "Ref", - Fields: []*api.IssueFormField{ - { - Type: "markdown", - ID: "id1", - Attributes: map[string]interface{}{ - "value": "Value of the markdown", - }, - }, - { - Type: "textarea", - ID: "id2", - Attributes: map[string]interface{}{ - "label": "Label of textarea", - "description": "Description of textarea", - "placeholder": "Placeholder of textarea", - "value": "Value of textarea", - "render": "bash", - }, - Validations: map[string]interface{}{ - "required": true, - }, - }, - { - Type: "input", - ID: "id3", - Attributes: map[string]interface{}{ - "label": "Label of input", - "description": "Description of input", - "placeholder": "Placeholder of input", - "value": "Value of input", - }, - Validations: map[string]interface{}{ - "required": true, - "is_number": true, - "regex": "[a-zA-Z0-9]+", - }, - }, - { - Type: "dropdown", - ID: "id4", - Attributes: map[string]interface{}{ - "label": "Label of dropdown", - "description": "Description of dropdown", - "multiple": true, - "options": []interface{}{ - "Option 1 of dropdown", - "Option 2 of dropdown", - "Option 3 of dropdown", +`, + want: &api.IssueTemplate{ + Name: "Name", + Title: "Title", + About: "About", + Labels: []string{"label1", "label2"}, + Ref: "Ref", + Fields: []*api.IssueFormField{ + { + Type: "markdown", + ID: "id1", + Attributes: map[string]interface{}{ + "value": "Value of the markdown", }, }, - Validations: map[string]interface{}{ - "required": true, + { + Type: "textarea", + ID: "id2", + Attributes: map[string]interface{}{ + "label": "Label of textarea", + "description": "Description of textarea", + "placeholder": "Placeholder of textarea", + "value": "Value of textarea", + "render": "bash", + }, + Validations: map[string]interface{}{ + "required": true, + }, }, - }, - { - Type: "checkboxes", - ID: "id5", - Attributes: map[string]interface{}{ - "label": "Label of checkboxes", - "description": "Description of checkboxes", - "options": []interface{}{ - map[interface{}]interface{}{"label": "Option 1 of checkboxes", "required": true}, - map[interface{}]interface{}{"label": "Option 2 of checkboxes", "required": false}, - map[interface{}]interface{}{"label": "Option 3 of checkboxes", "required": true}, + { + Type: "input", + ID: "id3", + Attributes: map[string]interface{}{ + "label": "Label of input", + "description": "Description of input", + "placeholder": "Placeholder of input", + "value": "Value of input", + }, + Validations: map[string]interface{}{ + "required": true, + "is_number": true, + "regex": "[a-zA-Z0-9]+", + }, + }, + { + Type: "dropdown", + ID: "id4", + Attributes: map[string]interface{}{ + "label": "Label of dropdown", + "description": "Description of dropdown", + "multiple": true, + "options": []interface{}{ + "Option 1 of dropdown", + "Option 2 of dropdown", + "Option 3 of dropdown", + }, + }, + Validations: map[string]interface{}{ + "required": true, + }, + }, + { + Type: "checkboxes", + ID: "id5", + Attributes: map[string]interface{}{ + "label": "Label of checkboxes", + "description": "Description of checkboxes", + "options": []interface{}{ + map[string]interface{}{"label": "Option 1 of checkboxes", "required": true}, + map[string]interface{}{"label": "Option 2 of checkboxes", "required": false}, + map[string]interface{}{"label": "Option 3 of checkboxes", "required": true}, + }, }, }, }, + FileName: "test.yaml", }, - FileName: "test.yaml", - } - got, err := unmarshal("test.yaml", []byte(content)) - if err != nil { - t.Fatal(err) - } - if err := Validate(got); err != nil { - t.Errorf("Validate() error = %v", err) - } - if !reflect.DeepEqual(want, got) { - jsonWant, _ := json.Marshal(want) - jsonGot, _ := json.Marshal(got) - t.Errorf("want:\n%s\ngot:\n%s", jsonWant, jsonGot) - } - }) + wantErr: "", + }, + { + name: "single label", + content: ` +name: Name +title: Title +about: About +labels: label1 +ref: Ref +body: + - type: markdown + id: id1 + attributes: + value: Value of the markdown +`, + want: &api.IssueTemplate{ + Name: "Name", + Title: "Title", + About: "About", + Labels: []string{"label1"}, + Ref: "Ref", + Fields: []*api.IssueFormField{ + { + Type: "markdown", + ID: "id1", + Attributes: map[string]interface{}{ + "value": "Value of the markdown", + }, + }, + }, + FileName: "test.yaml", + }, + wantErr: "", + }, + { + name: "comma-delimited labels", + content: ` +name: Name +title: Title +about: About +labels: label1,label2,,label3 ,, +ref: Ref +body: + - type: markdown + id: id1 + attributes: + value: Value of the markdown +`, + want: &api.IssueTemplate{ + Name: "Name", + Title: "Title", + About: "About", + Labels: []string{"label1", "label2", "label3"}, + Ref: "Ref", + Fields: []*api.IssueFormField{ + { + Type: "markdown", + ID: "id1", + Attributes: map[string]interface{}{ + "value": "Value of the markdown", + }, + }, + }, + FileName: "test.yaml", + }, + wantErr: "", + }, + { + name: "empty string as labels", + content: ` +name: Name +title: Title +about: About +labels: '' +ref: Ref +body: + - type: markdown + id: id1 + attributes: + value: Value of the markdown +`, + want: &api.IssueTemplate{ + Name: "Name", + Title: "Title", + About: "About", + Labels: nil, + Ref: "Ref", + Fields: []*api.IssueFormField{ + { + Type: "markdown", + ID: "id1", + Attributes: map[string]interface{}{ + "value": "Value of the markdown", + }, + }, + }, + FileName: "test.yaml", + }, + wantErr: "", + }, + { + name: "comma delimited labels in markdown", + filename: "test.md", + content: `--- +name: Name +title: Title +about: About +labels: label1,label2,,label3 ,, +ref: Ref +--- +Content +`, + want: &api.IssueTemplate{ + Name: "Name", + Title: "Title", + About: "About", + Labels: []string{"label1", "label2", "label3"}, + Ref: "Ref", + Fields: nil, + Content: "Content\n", + FileName: "test.md", + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filename := "test.yaml" + if tt.filename != "" { + filename = tt.filename + } + tmpl, err := unmarshal(filename, []byte(tt.content)) + require.NoError(t, err) + if tt.wantErr != "" { + require.EqualError(t, Validate(tmpl), tt.wantErr) + } else { + require.NoError(t, Validate(tmpl)) + want, _ := json.Marshal(tt.want) + got, _ := json.Marshal(tmpl) + require.JSONEq(t, string(want), string(got)) + } + }) + } } func TestRenderToMarkdown(t *testing.T) { diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index e695d1e1cc..9b684f1bf7 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -7,15 +7,16 @@ package template import ( "fmt" "io" - "path/filepath" + "path" "strconv" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) // CouldBe indicates a file with the filename could be a template, @@ -43,7 +44,7 @@ func Unmarshal(filename string, content []byte) (*api.IssueTemplate, error) { // UnmarshalFromEntry parses out a valid template from the blob in entry func UnmarshalFromEntry(entry *git.TreeEntry, dir string) (*api.IssueTemplate, error) { - return unmarshalFromEntry(entry, filepath.Join(dir, entry.Name())) + return unmarshalFromEntry(entry, path.Join(dir, entry.Name())) // Filepaths in Git are ALWAYS '/' separated do not use filepath here } // UnmarshalFromCommit parses out a valid template from the commit @@ -95,14 +96,27 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) { }{} if typ := it.Type(); typ == api.IssueTemplateTypeMarkdown { - templateBody, err := markdown.ExtractMetadata(string(content), it) - if err != nil { - return nil, err - } - it.Content = templateBody - if it.About == "" { - if _, err := markdown.ExtractMetadata(string(content), compatibleTemplate); err == nil && compatibleTemplate.About != "" { - it.About = compatibleTemplate.About + if templateBody, err := markdown.ExtractMetadata(string(content), it); err != nil { + // The only thing we know here is that we can't extract metadata from the content, + // it's hard to tell if metadata doesn't exist or metadata isn't valid. + // There's an example template: + // + // --- + // # Title + // --- + // Content + // + // It could be a valid markdown with two horizontal lines, or an invalid markdown with wrong metadata. + + it.Content = string(content) + it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath! + it.About, _ = util.SplitStringAtByteN(it.Content, 80) + } else { + it.Content = templateBody + if it.About == "" { + if _, err := markdown.ExtractMetadata(string(content), compatibleTemplate); err == nil && compatibleTemplate.About != "" { + it.About = compatibleTemplate.About + } } } } else if typ == api.IssueTemplateTypeYaml { diff --git a/modules/lfs/endpoint.go b/modules/lfs/endpoint.go index 943966ed15..15c3d908a1 100644 --- a/modules/lfs/endpoint.go +++ b/modules/lfs/endpoint.go @@ -5,7 +5,6 @@ package lfs import ( - "fmt" "net/url" "os" "path" @@ -13,6 +12,7 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) // DetermineEndpoint determines an endpoint from the clone url or uses the specified LFS url. @@ -96,7 +96,7 @@ func endpointFromLocalPath(path string) *url.URL { return nil } - path = fmt.Sprintf("file://%s%s", slash, filepath.ToSlash(path)) + path = "file://" + slash + util.PathEscapeSegments(filepath.ToSlash(path)) u, _ := url.Parse(path) diff --git a/modules/log/groutinelabel.go b/modules/log/groutinelabel.go index 0d3739fd98..c83880f4de 100644 --- a/modules/log/groutinelabel.go +++ b/modules/log/groutinelabel.go @@ -7,7 +7,7 @@ package log import "unsafe" //go:linkname runtime_getProfLabel runtime/pprof.runtime_getProfLabel -func runtime_getProfLabel() unsafe.Pointer // nolint +func runtime_getProfLabel() unsafe.Pointer //nolint type labelMap map[string]string diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index 0eeb2d70a5..230cbccaac 100644 --- a/modules/markup/external/external.go +++ b/modules/markup/external/external.go @@ -5,6 +5,7 @@ package external import ( + "bytes" "fmt" "io" "os" @@ -133,11 +134,13 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io. if !p.IsInputFile { cmd.Stdin = input } + var stderr bytes.Buffer cmd.Stdout = output + cmd.Stderr = &stderr process.SetSysProcAttribute(cmd) if err := cmd.Run(); err != nil { - return fmt.Errorf("%s render run command %s %v failed: %w", p.Name(), commands[0], args, err) + return fmt.Errorf("%s render run command %s %v failed: %w\nStderr: %s", p.Name(), commands[0], args, err, stderr.String()) } return nil } diff --git a/modules/markup/html.go b/modules/markup/html.go index ae00c3905f..3838708231 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -358,12 +358,19 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output } func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node) { - // Add user-content- to IDs if they don't already have them + // Add user-content- to IDs and "#" links if they don't already have them for idx, attr := range node.Attr { - if attr.Key == "id" && !(strings.HasPrefix(attr.Val, "user-content-") || blackfridayExtRegex.MatchString(attr.Val)) { + val := strings.TrimPrefix(attr.Val, "#") + notHasPrefix := !(strings.HasPrefix(val, "user-content-") || blackfridayExtRegex.MatchString(val)) + + if attr.Key == "id" && notHasPrefix { node.Attr[idx].Val = "user-content-" + attr.Val } + if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix { + node.Attr[idx].Val = "#user-content-" + val + } + if attr.Key == "class" && attr.Val == "emoji" { textProcs = nil } diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go index 720d0066f4..1e9768e618 100644 --- a/modules/markup/markdown/meta_test.go +++ b/modules/markup/markdown/meta_test.go @@ -9,82 +9,86 @@ import ( "strings" "testing" - "code.gitea.io/gitea/modules/structs" - "github.com/stretchr/testify/assert" ) -func validateMetadata(it structs.IssueTemplate) bool { - /* - A legacy to keep the unit tests working. - Copied from the method "func (it IssueTemplate) Valid() bool", the original method has been removed. - Because it becomes quite complicated to validate an issue template which is support yaml form now. - The new way to validate an issue template is to call the Validate in modules/issue/template, - */ +/* +IssueTemplate is a legacy to keep the unit tests working. +Copied from structs.IssueTemplate, the original type has been changed a lot to support yaml template. +*/ +type IssueTemplate struct { + Name string `json:"name" yaml:"name"` + Title string `json:"title" yaml:"title"` + About string `json:"about" yaml:"about"` + Labels []string `json:"labels" yaml:"labels"` + Ref string `json:"ref" yaml:"ref"` +} + +func (it *IssueTemplate) Valid() bool { return strings.TrimSpace(it.Name) != "" && strings.TrimSpace(it.About) != "" } func TestExtractMetadata(t *testing.T) { t.Run("ValidFrontAndBody", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate body, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest), &meta) assert.NoError(t, err) assert.Equal(t, bodyTest, body) assert.Equal(t, metaTest, meta) - assert.True(t, validateMetadata(meta)) + assert.True(t, meta.Valid()) }) t.Run("NoFirstSeparator", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate _, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest), &meta) assert.Error(t, err) }) t.Run("NoLastSeparator", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate _, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest), &meta) assert.Error(t, err) }) t.Run("NoBody", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate body, err := ExtractMetadata(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest), &meta) assert.NoError(t, err) assert.Equal(t, "", body) assert.Equal(t, metaTest, meta) - assert.True(t, validateMetadata(meta)) + assert.True(t, meta.Valid()) }) } func TestExtractMetadataBytes(t *testing.T) { t.Run("ValidFrontAndBody", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) assert.NoError(t, err) assert.Equal(t, bodyTest, string(body)) assert.Equal(t, metaTest, meta) - assert.True(t, validateMetadata(meta)) + assert.True(t, meta.Valid()) }) t.Run("NoFirstSeparator", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta) assert.Error(t, err) }) t.Run("NoLastSeparator", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta) assert.Error(t, err) }) t.Run("NoBody", func(t *testing.T) { - var meta structs.IssueTemplate + var meta IssueTemplate body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) assert.NoError(t, err) assert.Equal(t, "", string(body)) assert.Equal(t, metaTest, meta) - assert.True(t, validateMetadata(meta)) + assert.True(t, meta.Valid()) }) } @@ -97,7 +101,7 @@ labels: - bug - "test label"` bodyTest = "This is the body" - metaTest = structs.IssueTemplate{ + metaTest = IssueTemplate{ Name: "Test", About: "A Test", Title: "Test Title", diff --git a/modules/migration/comment.go b/modules/migration/comment.go index 0447689b74..c7494d2380 100644 --- a/modules/migration/comment.go +++ b/modules/migration/comment.go @@ -9,8 +9,7 @@ import "time" // Commentable can be commented upon type Commentable interface { - GetLocalIndex() int64 - GetForeignIndex() int64 + Reviewable GetContext() DownloaderContext } diff --git a/modules/migration/issue.go b/modules/migration/issue.go index cc13570afb..9bc7a0ee90 100644 --- a/modules/migration/issue.go +++ b/modules/migration/issue.go @@ -35,6 +35,15 @@ func (issue *Issue) GetExternalName() string { return issue.PosterName } // GetExternalID ExternalUserMigrated interface func (issue *Issue) GetExternalID() int64 { return issue.PosterID } -func (issue *Issue) GetLocalIndex() int64 { return issue.Number } -func (issue *Issue) GetForeignIndex() int64 { return issue.ForeignIndex } +func (issue *Issue) GetLocalIndex() int64 { return issue.Number } + +func (issue *Issue) GetForeignIndex() int64 { + // see the comment of Reviewable.GetForeignIndex + // if there is no ForeignIndex, then use LocalIndex + if issue.ForeignIndex == 0 { + return issue.Number + } + return issue.ForeignIndex +} + func (issue *Issue) GetContext() DownloaderContext { return issue.Context } diff --git a/modules/migration/review.go b/modules/migration/review.go index b5a054c642..dcef8e0f51 100644 --- a/modules/migration/review.go +++ b/modules/migration/review.go @@ -9,6 +9,16 @@ import "time" // Reviewable can be reviewed type Reviewable interface { GetLocalIndex() int64 + + // GetForeignIndex presents the foreign index, which could be misused: + // For example, if there are 2 Gitea sites: site-A exports a dataset, then site-B imports it: + // * if site-A exports files by using its LocalIndex + // * from site-A's view, LocalIndex is site-A's IssueIndex while ForeignIndex is site-B's IssueIndex + // * but from site-B's view, LocalIndex is site-B's IssueIndex while ForeignIndex is site-A's IssueIndex + // + // So the exporting/importing must be paired, but the meaning of them looks confusing then: + // * either site-A and site-B both use LocalIndex during dumping/restoring + // * or site-A and site-B both use ForeignIndex GetForeignIndex() int64 } @@ -38,7 +48,7 @@ type Review struct { // GetExternalName ExternalUserMigrated interface func (r *Review) GetExternalName() string { return r.ReviewerName } -// ExternalID ExternalUserMigrated interface +// GetExternalID ExternalUserMigrated interface func (r *Review) GetExternalID() int64 { return r.ReviewerID } // ReviewComment represents a review comment diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go index 4d96a6b0ed..3914e0e85c 100644 --- a/modules/notification/ui/ui.go +++ b/modules/notification/ui/ui.go @@ -96,6 +96,7 @@ func (ns *notificationService) NotifyIssueChangeStatus(doer *user_model.User, is _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: issue.ID, NotificationAuthorID: doer.ID, + CommentID: actionComment.ID, }) } diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index fd38e67859..dd0c28ce78 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -11,8 +11,9 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/packages/container/helm" - "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/validation" + + oci "github.com/opencontainers/image-spec/specs-go/v1" ) const ( @@ -66,8 +67,8 @@ type Metadata struct { } // ParseImageConfig parses the metadata of an image config -func ParseImageConfig(mediaType oci.MediaType, r io.Reader) (*Metadata, error) { - if strings.EqualFold(string(mediaType), helm.ConfigMediaType) { +func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) { + if strings.EqualFold(mt, helm.ConfigMediaType) { return parseHelmConfig(r) } diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index 9400cf6954..701d2b548e 100644 --- a/modules/packages/container/metadata_test.go +++ b/modules/packages/container/metadata_test.go @@ -9,8 +9,8 @@ import ( "testing" "code.gitea.io/gitea/modules/packages/container/helm" - "code.gitea.io/gitea/modules/packages/container/oci" + oci "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" ) @@ -24,7 +24,7 @@ func TestParseImageConfig(t *testing.T) { configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}` - metadata, err := ParseImageConfig(oci.MediaType(oci.MediaTypeImageManifest), strings.NewReader(configOCI)) + metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI)) assert.NoError(t, err) assert.Equal(t, TypeOCI, metadata.Type) @@ -51,7 +51,7 @@ func TestParseImageConfig(t *testing.T) { configHelm := `{"description":"` + description + `", "home": "` + projectURL + `", "sources": ["` + repositoryURL + `"], "maintainers":[{"name":"` + author + `"}]}` - metadata, err = ParseImageConfig(oci.MediaType(helm.ConfigMediaType), strings.NewReader(configHelm)) + metadata, err = ParseImageConfig(helm.ConfigMediaType, strings.NewReader(configHelm)) assert.NoError(t, err) assert.Equal(t, TypeHelm, metadata.Type) diff --git a/modules/packages/container/oci/digest.go b/modules/packages/container/oci/digest.go deleted file mode 100644 index 5234814cfe..0000000000 --- a/modules/packages/container/oci/digest.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package oci - -import ( - "regexp" - "strings" -) - -var digestPattern = regexp.MustCompile(`\Asha256:[a-f0-9]{64}\z`) - -type Digest string - -// Validate checks if the digest has a valid SHA256 signature -func (d Digest) Validate() bool { - return digestPattern.MatchString(string(d)) -} - -func (d Digest) Hash() string { - p := strings.SplitN(string(d), ":", 2) - if len(p) != 2 { - return "" - } - return p[1] -} diff --git a/modules/packages/container/oci/mediatype.go b/modules/packages/container/oci/mediatype.go deleted file mode 100644 index 2636fbe288..0000000000 --- a/modules/packages/container/oci/mediatype.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package oci - -import ( - "strings" -) - -const ( - MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json" - MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json" - MediaTypeDockerManifest = "application/vnd.docker.distribution.manifest.v2+json" - MediaTypeDockerManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" -) - -type MediaType string - -// IsValid tests if the media type is in the OCI or Docker namespace -func (m MediaType) IsValid() bool { - s := string(m) - return strings.HasPrefix(s, "application/vnd.docker.") || strings.HasPrefix(s, "application/vnd.oci.") -} - -// IsImageManifest tests if the media type is an image manifest -func (m MediaType) IsImageManifest() bool { - s := string(m) - return strings.EqualFold(s, MediaTypeDockerManifest) || strings.EqualFold(s, MediaTypeImageManifest) -} - -// IsImageIndex tests if the media type is an image index -func (m MediaType) IsImageIndex() bool { - s := string(m) - return strings.EqualFold(s, MediaTypeDockerManifestList) || strings.EqualFold(s, MediaTypeImageIndex) -} diff --git a/modules/packages/container/oci/oci.go b/modules/packages/container/oci/oci.go deleted file mode 100644 index 01cca8fe69..0000000000 --- a/modules/packages/container/oci/oci.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package oci - -import ( - "time" -) - -// https://github.com/opencontainers/image-spec/tree/main/specs-go/v1 - -// ImageConfig defines the execution parameters which should be used as a base when running a container using an image. -type ImageConfig struct { - // User defines the username or UID which the process in the container should run as. - User string `json:"User,omitempty"` - - // ExposedPorts a set of ports to expose from a container running this image. - ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"` - - // Env is a list of environment variables to be used in a container. - Env []string `json:"Env,omitempty"` - - // Entrypoint defines a list of arguments to use as the command to execute when the container starts. - Entrypoint []string `json:"Entrypoint,omitempty"` - - // Cmd defines the default arguments to the entrypoint of the container. - Cmd []string `json:"Cmd,omitempty"` - - // Volumes is a set of directories describing where the process is likely write data specific to a container instance. - Volumes map[string]struct{} `json:"Volumes,omitempty"` - - // WorkingDir sets the current working directory of the entrypoint process in the container. - WorkingDir string `json:"WorkingDir,omitempty"` - - // Labels contains arbitrary metadata for the container. - Labels map[string]string `json:"Labels,omitempty"` - - // StopSignal contains the system call signal that will be sent to the container to exit. - StopSignal string `json:"StopSignal,omitempty"` -} - -// RootFS describes a layer content addresses -type RootFS struct { - // Type is the type of the rootfs. - Type string `json:"type"` - - // DiffIDs is an array of layer content hashes, in order from bottom-most to top-most. - DiffIDs []string `json:"diff_ids"` -} - -// History describes the history of a layer. -type History struct { - // Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6. - Created *time.Time `json:"created,omitempty"` - - // CreatedBy is the command which created the layer. - CreatedBy string `json:"created_by,omitempty"` - - // Author is the author of the build point. - Author string `json:"author,omitempty"` - - // Comment is a custom message set when creating the layer. - Comment string `json:"comment,omitempty"` - - // EmptyLayer is used to mark if the history item created a filesystem diff. - EmptyLayer bool `json:"empty_layer,omitempty"` -} - -// Image is the JSON structure which describes some basic information about the image. -// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON. -type Image struct { - // Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6. - Created *time.Time `json:"created,omitempty"` - - // Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image. - Author string `json:"author,omitempty"` - - // Architecture is the CPU architecture which the binaries in this image are built to run on. - Architecture string `json:"architecture"` - - // Variant is the variant of the specified CPU architecture which image binaries are intended to run on. - Variant string `json:"variant,omitempty"` - - // OS is the name of the operating system which the image is built to run on. - OS string `json:"os"` - - // OSVersion is an optional field specifying the operating system - // version, for example on Windows `10.0.14393.1066`. - OSVersion string `json:"os.version,omitempty"` - - // OSFeatures is an optional field specifying an array of strings, - // each listing a required OS feature (for example on Windows `win32k`). - OSFeatures []string `json:"os.features,omitempty"` - - // Config defines the execution parameters which should be used as a base when running a container using the image. - Config ImageConfig `json:"config,omitempty"` - - // RootFS references the layer content addresses used by the image. - RootFS RootFS `json:"rootfs"` - - // History describes the history of each layer. - History []History `json:"history,omitempty"` -} - -// Descriptor describes the disposition of targeted content. -// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype -// when marshalled to JSON. -type Descriptor struct { - // MediaType is the media type of the object this schema refers to. - MediaType MediaType `json:"mediaType,omitempty"` - - // Digest is the digest of the targeted content. - Digest Digest `json:"digest"` - - // Size specifies the size in bytes of the blob. - Size int64 `json:"size"` - - // URLs specifies a list of URLs from which this object MAY be downloaded - URLs []string `json:"urls,omitempty"` - - // Annotations contains arbitrary metadata relating to the targeted content. - Annotations map[string]string `json:"annotations,omitempty"` - - // Data is an embedding of the targeted content. This is encoded as a base64 - // string when marshalled to JSON (automatically, by encoding/json). If - // present, Data can be used directly to avoid fetching the targeted content. - Data []byte `json:"data,omitempty"` - - // Platform describes the platform which the image in the manifest runs on. - // - // This should only be used when referring to a manifest. - Platform *Platform `json:"platform,omitempty"` -} - -// Platform describes the platform which the image in the manifest runs on. -type Platform struct { - // Architecture field specifies the CPU architecture, for example - // `amd64` or `ppc64`. - Architecture string `json:"architecture"` - - // OS specifies the operating system, for example `linux` or `windows`. - OS string `json:"os"` - - // OSVersion is an optional field specifying the operating system - // version, for example on Windows `10.0.14393.1066`. - OSVersion string `json:"os.version,omitempty"` - - // OSFeatures is an optional field specifying an array of strings, - // each listing a required OS feature (for example on Windows `win32k`). - OSFeatures []string `json:"os.features,omitempty"` - - // Variant is an optional field specifying a variant of the CPU, for - // example `v7` to specify ARMv7 when architecture is `arm`. - Variant string `json:"variant,omitempty"` -} - -type SchemaMediaBase struct { - // SchemaVersion is the image manifest schema that this image follows - SchemaVersion int `json:"schemaVersion"` - - // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.manifest.v1+json` - MediaType MediaType `json:"mediaType,omitempty"` -} - -// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON. -type Manifest struct { - SchemaMediaBase - - // Config references a configuration object for a container, by digest. - // The referenced configuration object is a JSON blob that the runtime uses to set up the container. - Config Descriptor `json:"config"` - - // Layers is an indexed list of layers referenced by the manifest. - Layers []Descriptor `json:"layers"` - - // Annotations contains arbitrary metadata for the image manifest. - Annotations map[string]string `json:"annotations,omitempty"` -} - -// Index references manifests for various platforms. -// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON. -type Index struct { - SchemaMediaBase - - // Manifests references platform specific manifests. - Manifests []Descriptor `json:"manifests"` - - // Annotations contains arbitrary metadata for the image index. - Annotations map[string]string `json:"annotations,omitempty"` -} diff --git a/modules/packages/container/oci/reference.go b/modules/packages/container/oci/reference.go deleted file mode 100644 index 120ff122d4..0000000000 --- a/modules/packages/container/oci/reference.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package oci - -import ( - "regexp" -) - -var referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) - -type Reference string - -func (r Reference) Validate() bool { - return referencePattern.MatchString(string(r)) -} diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go index a3a5d1a666..b0e653a102 100644 --- a/modules/packages/content_store.go +++ b/modules/packages/content_store.go @@ -30,6 +30,13 @@ func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) { return s.store.Open(KeyToRelativePath(key)) } +// FIXME: Workaround to be removed in v1.20 +// https://github.com/go-gitea/gitea/issues/19586 +func (s *ContentStore) Has(key BlobHash256Key) error { + _, err := s.store.Stat(KeyToRelativePath(key)) + return err +} + // Save stores a package blob func (s *ContentStore) Save(key BlobHash256Key, r io.Reader, size int64) error { _, err := s.store.Save(KeyToRelativePath(key), r, size) diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 2b555e47e9..98f5cac17a 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -6,8 +6,10 @@ package nuget import ( "archive/zip" + "bytes" "encoding/xml" "errors" + "fmt" "io" "path/filepath" "regexp" @@ -183,7 +185,23 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) { return &Package{ PackageType: packageType, ID: p.Metadata.ID, - Version: v.String(), + Version: toNormalizedVersion(v), Metadata: m, }, nil } + +// https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers +// https://github.com/NuGet/NuGet.Client/blob/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Versioning/VersionFormatter.cs#L121 +func toNormalizedVersion(v *version.Version) string { + var buf bytes.Buffer + segments := v.Segments64() + fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) + if len(segments) > 3 && segments[3] > 0 { + fmt.Fprintf(&buf, ".%d", segments[3]) + } + pre := v.Prerelease() + if pre != "" { + fmt.Fprint(&buf, "-", pre) + } + return buf.String() +} diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go index e8c7773e97..2fb517504c 100644 --- a/modules/packages/nuget/metadata_test.go +++ b/modules/packages/nuget/metadata_test.go @@ -147,6 +147,19 @@ func TestParseNuspecMetaData(t *testing.T) { assert.Len(t, deps, 1) assert.Equal(t, dependencyID, deps[0].ID) assert.Equal(t, dependencyVersion, deps[0].Version) + + t.Run("NormalizedVersion", func(t *testing.T) { + np, err := ParseNuspecMetaData(strings.NewReader(` + + + test + 1.04.5.2.5-rc.1+metadata + +`)) + assert.NoError(t, err) + assert.NotNil(t, np) + assert.Equal(t, "1.4.5.2-rc.1", np.Version) + }) }) t.Run("Symbols Package", func(t *testing.T) { diff --git a/modules/proxy/proxy.go b/modules/proxy/proxy.go index 61730544b0..1dea0cf450 100644 --- a/modules/proxy/proxy.go +++ b/modules/proxy/proxy.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "os" + "strings" "sync" "code.gitea.io/gitea/modules/log" @@ -83,3 +84,16 @@ func Proxy() func(req *http.Request) (*url.URL, error) { return http.ProxyFromEnvironment(req) } } + +// EnvWithProxy returns os.Environ(), with a https_proxy env, if the given url +// needs to be proxied. +func EnvWithProxy(u *url.URL) []string { + envs := os.Environ() + if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") { + if Match(u.Host) { + envs = append(envs, "https_proxy="+GetProxyURL()) + } + } + + return envs +} diff --git a/modules/queue/queue_channel.go b/modules/queue/queue_channel.go index 028023d500..3a375fdb41 100644 --- a/modules/queue/queue_channel.go +++ b/modules/queue/queue_channel.go @@ -110,32 +110,6 @@ func (q *ChannelQueue) Flush(timeout time.Duration) error { return q.FlushWithContext(ctx) } -// FlushWithContext is very similar to CleanUp but it will return as soon as the dataChan is empty -func (q *ChannelQueue) FlushWithContext(ctx context.Context) error { - log.Trace("ChannelQueue: %d Flush", q.qid) - paused, _ := q.IsPausedIsResumed() - for { - select { - case <-paused: - return nil - case data, ok := <-q.dataChan: - if !ok { - return nil - } - if unhandled := q.handle(data); unhandled != nil { - log.Error("Unhandled Data whilst flushing queue %d", q.qid) - } - atomic.AddInt64(&q.numInQueue, -1) - case <-q.baseCtx.Done(): - return q.baseCtx.Err() - case <-ctx.Done(): - return ctx.Err() - default: - return nil - } - } -} - // Shutdown processing from this queue func (q *ChannelQueue) Shutdown() { q.lock.Lock() diff --git a/modules/queue/unique_queue_channel.go b/modules/queue/unique_queue_channel.go index d1bf7239eb..8458e8c52e 100644 --- a/modules/queue/unique_queue_channel.go +++ b/modules/queue/unique_queue_channel.go @@ -9,7 +9,6 @@ import ( "fmt" "runtime/pprof" "sync" - "sync/atomic" "time" "code.gitea.io/gitea/modules/container" @@ -168,35 +167,6 @@ func (q *ChannelUniqueQueue) Flush(timeout time.Duration) error { return q.FlushWithContext(ctx) } -// FlushWithContext is very similar to CleanUp but it will return as soon as the dataChan is empty -func (q *ChannelUniqueQueue) FlushWithContext(ctx context.Context) error { - log.Trace("ChannelUniqueQueue: %d Flush", q.qid) - paused, _ := q.IsPausedIsResumed() - for { - select { - case <-paused: - return nil - default: - } - select { - case data, ok := <-q.dataChan: - if !ok { - return nil - } - if unhandled := q.handle(data); unhandled != nil { - log.Error("Unhandled Data whilst flushing queue %d", q.qid) - } - atomic.AddInt64(&q.numInQueue, -1) - case <-q.baseCtx.Done(): - return q.baseCtx.Err() - case <-ctx.Done(): - return ctx.Err() - default: - return nil - } - } -} - // Shutdown processing from this queue func (q *ChannelUniqueQueue) Shutdown() { log.Trace("ChannelUniqueQueue: %s Shutting down", q.name) diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go index bdf04a363b..9403bfddec 100644 --- a/modules/queue/workerpool.go +++ b/modules/queue/workerpool.go @@ -464,13 +464,43 @@ func (p *WorkerPool) IsEmpty() bool { return atomic.LoadInt64(&p.numInQueue) == 0 } +// contextError returns either ctx.Done(), the base context's error or nil +func (p *WorkerPool) contextError(ctx context.Context) error { + select { + case <-p.baseCtx.Done(): + return p.baseCtx.Err() + case <-ctx.Done(): + return ctx.Err() + default: + return nil + } +} + // FlushWithContext is very similar to CleanUp but it will return as soon as the dataChan is empty // NB: The worker will not be registered with the manager. func (p *WorkerPool) FlushWithContext(ctx context.Context) error { log.Trace("WorkerPool: %d Flush", p.qid) + paused, _ := p.IsPausedIsResumed() for { + // Because select will return any case that is satisified at random we precheck here before looking at dataChan. select { - case data := <-p.dataChan: + case <-paused: + // Ensure that even if paused that the cancelled error is still sent + return p.contextError(ctx) + case <-p.baseCtx.Done(): + return p.baseCtx.Err() + case <-ctx.Done(): + return ctx.Err() + default: + } + + select { + case <-paused: + return p.contextError(ctx) + case data, ok := <-p.dataChan: + if !ok { + return nil + } if unhandled := p.handle(data); unhandled != nil { log.Error("Unhandled Data whilst flushing queue %d", p.qid) } @@ -496,6 +526,7 @@ func (p *WorkerPool) doWork(ctx context.Context) { paused, _ := p.IsPausedIsResumed() data := make([]Data, 0, p.batchLength) for { + // Because select will return any case that is satisified at random we precheck here before looking at dataChan. select { case <-paused: log.Trace("Worker for Queue %d Pausing", p.qid) @@ -516,8 +547,19 @@ func (p *WorkerPool) doWork(ctx context.Context) { log.Trace("Worker shutting down") return } + case <-ctx.Done(): + if len(data) > 0 { + log.Trace("Handling: %d data, %v", len(data), data) + if unhandled := p.handle(data...); unhandled != nil { + log.Error("Unhandled Data in queue %d", p.qid) + } + atomic.AddInt64(&p.numInQueue, -1*int64(len(data))) + } + log.Trace("Worker shutting down") + return default: } + select { case <-paused: // go back around diff --git a/modules/repository/create.go b/modules/repository/create.go index 1fec0335a2..d4269ff8b8 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path" + "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -286,9 +287,36 @@ func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_m return repo, nil } -// UpdateRepoSize updates the repository size, calculating it using util.GetDirectorySize +const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular + +// getDirectorySize returns the disk consumption for a given path +func getDirectorySize(path string) (int64, error) { + var size int64 + err := filepath.WalkDir(path, func(_ string, info os.DirEntry, err error) error { + if err != nil { + if os.IsNotExist(err) { // ignore the error because the file maybe deleted during traversing. + return nil + } + return err + } + if info.IsDir() { + return nil + } + f, err := info.Info() + if err != nil { + return err + } + if (f.Mode() & notRegularFileMode) == 0 { + size += f.Size() + } + return err + }) + return size, err +} + +// UpdateRepoSize updates the repository size, calculating it using getDirectorySize func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error { - size, err := util.GetDirectorySize(repo.RepoPath()) + size, err := getDirectorySize(repo.RepoPath()) if err != nil { return fmt.Errorf("updateSize: %w", err) } diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index 3040782845..ff70468db1 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -169,3 +169,13 @@ func TestUpdateRepositoryVisibilityChanged(t *testing.T) { assert.NoError(t, err) assert.True(t, act.IsPrivate) } + +func TestGetDirectorySize(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo, err := repo_model.GetRepositoryByID(1) + assert.NoError(t, err) + + size, err := getDirectorySize(repo.RepoPath()) + assert.NoError(t, err) + assert.EqualValues(t, size, repo.Size) +} diff --git a/modules/repository/init.go b/modules/repository/init.go index 65072a9599..c1b97069ff 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -319,7 +319,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi cmd := git.NewCommand(ctx, "commit", git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email)), - "-m", "Initial commit", + "--message=Initial commit", ) sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) diff --git a/modules/setting/database.go b/modules/setting/database.go index 4e55457395..d8d4d12d7a 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -90,7 +90,7 @@ func InitDBConfig() { log.Error("Deprecated database mysql charset utf8 support, please use utf8mb4 or convert utf8 to utf8mb4.") } - Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) + Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "forgejo.db")) Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) Database.SQLiteJournalMode = sec.Key("SQLITE_JOURNAL_MODE").MustString("") diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index d6f1dae0f7..4dc16bebaf 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -13,78 +13,57 @@ import ( "code.gitea.io/gitea/modules/log" shellquote "github.com/kballard/go-shellquote" + ini "gopkg.in/ini.v1" ) // Mailer represents mail service. type Mailer struct { // Mailer - Name string - From string - EnvelopeFrom string - OverrideEnvelopeFrom bool `ini:"-"` - FromName string - FromEmail string - SendAsPlainText bool - SubjectPrefix string + Name string `ini:"NAME"` + From string `ini:"FROM"` + EnvelopeFrom string `ini:"ENVELOPE_FROM"` + OverrideEnvelopeFrom bool `ini:"-"` + FromName string `ini:"-"` + FromEmail string `ini:"-"` + SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"` + SubjectPrefix string `ini:"SUBJECT_PREFIX"` // SMTP sender - Protocol string - SMTPAddr string - SMTPPort string - User, Passwd string - EnableHelo bool - HeloHostname string - ForceTrustServerCert bool - UseClientCert bool - ClientCertFile string - ClientKeyFile string + Protocol string `ini:"PROTOCOL"` + SMTPAddr string `ini:"SMTP_ADDR"` + SMTPPort string `ini:"SMTP_PORT"` + User string `ini:"USER"` + Passwd string `ini:"PASSWD"` + EnableHelo bool `ini:"ENABLE_HELO"` + HeloHostname string `ini:"HELO_HOSTNAME"` + ForceTrustServerCert bool `ini:"FORCE_TRUST_SERVER_CERT"` + UseClientCert bool `ini:"USE_CLIENT_CERT"` + ClientCertFile string `ini:"CLIENT_CERT_FILE"` + ClientKeyFile string `ini:"CLIENT_KEY_FILE"` // Sendmail sender - SendmailPath string - SendmailArgs []string - SendmailTimeout time.Duration - SendmailConvertCRLF bool + SendmailPath string `ini:"SENDMAIL_PATH"` + SendmailArgs []string `ini:"-"` + SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"` + SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"` } // MailService the global mailer var MailService *Mailer -func newMailService() { - sec := Cfg.Section("mailer") +func parseMailerConfig(rootCfg *ini.File) { + sec := rootCfg.Section("mailer") // Check mailer setting. if !sec.Key("ENABLED").MustBool() { return } - MailService = &Mailer{ - Name: sec.Key("NAME").MustString(AppName), - SendAsPlainText: sec.Key("SEND_AS_PLAIN_TEXT").MustBool(false), - - Protocol: sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+startls", "smtp+unix", "sendmail", "dummy"}), - SMTPAddr: sec.Key("SMTP_ADDR").String(), - SMTPPort: sec.Key("SMTP_PORT").String(), - User: sec.Key("USER").String(), - Passwd: sec.Key("PASSWD").String(), - EnableHelo: sec.Key("ENABLE_HELO").MustBool(true), - HeloHostname: sec.Key("HELO_HOSTNAME").String(), - ForceTrustServerCert: sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false), - UseClientCert: sec.Key("USE_CLIENT_CERT").MustBool(false), - ClientCertFile: sec.Key("CLIENT_CERT_FILE").String(), - ClientKeyFile: sec.Key("CLIENT_KEY_FILE").String(), - SubjectPrefix: sec.Key("SUBJECT_PREFIX").MustString(""), - - SendmailPath: sec.Key("SENDMAIL_PATH").MustString("sendmail"), - SendmailTimeout: sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute), - SendmailConvertCRLF: sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true), - } - MailService.From = sec.Key("FROM").MustString(MailService.User) - MailService.EnvelopeFrom = sec.Key("ENVELOPE_FROM").MustString("") - + // Handle Deprecations and map on to new configuration // FIXME: DEPRECATED to be removed in v1.19.0 deprecatedSetting("mailer", "MAILER_TYPE", "mailer", "PROTOCOL") if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") { if sec.Key("MAILER_TYPE").String() == "sendmail" { - MailService.Protocol = "sendmail" + sec.Key("PROTOCOL").MustString("sendmail") } } @@ -93,34 +72,99 @@ func newMailService() { if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") { givenHost := sec.Key("HOST").String() addr, port, err := net.SplitHostPort(givenHost) - if err != nil { + if err != nil && strings.Contains(err.Error(), "missing port in address") { + addr = givenHost + } else if err != nil { log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err) } - MailService.SMTPAddr = addr - MailService.SMTPPort = port + if addr == "" { + addr = "127.0.0.1" + } + sec.Key("SMTP_ADDR").MustString(addr) + sec.Key("SMTP_PORT").MustString(port) } // FIXME: DEPRECATED to be removed in v1.19.0 deprecatedSetting("mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL") if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") { if sec.Key("IS_TLS_ENABLED").MustBool() { - MailService.Protocol = "smtps" + sec.Key("PROTOCOL").MustString("smtps") } else { - MailService.Protocol = "smtp+startls" + sec.Key("PROTOCOL").MustString("smtp+starttls") } } + // FIXME: DEPRECATED to be removed in v1.19.0 + deprecatedSetting("mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO") + if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") { + sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool()) + } + + // FIXME: DEPRECATED to be removed in v1.19.0 + deprecatedSetting("mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT") + if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") { + sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool()) + } + + // FIXME: DEPRECATED to be removed in v1.19.0 + deprecatedSetting("mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT") + if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") { + sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool()) + } + + // FIXME: DEPRECATED to be removed in v1.19.0 + deprecatedSetting("mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE") + if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") { + sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String()) + } + + // FIXME: DEPRECATED to be removed in v1.19.0 + deprecatedSetting("mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE") + if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") { + sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String()) + } + + // FIXME: DEPRECATED to be removed in v1.19.0 + deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT") + if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") { + sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false)) + } + + if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" { + log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls`` instead. This fallback will be removed in v1.19.0") + sec.Key("PROTOCOL").SetValue("smtp+starttls") + } + + // Set default values & validate + sec.Key("NAME").MustString(AppName) + sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy"}) + sec.Key("ENABLE_HELO").MustBool(true) + sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false) + sec.Key("USE_CLIENT_CERT").MustBool(false) + sec.Key("SENDMAIL_PATH").MustString("sendmail") + sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute) + sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true) + sec.Key("FROM").MustString(sec.Key("USER").String()) + + // Now map the values on to the MailService + MailService = &Mailer{} + if err := sec.MapTo(MailService); err != nil { + log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err) + } + + // Infer SMTPPort if not set if MailService.SMTPPort == "" { switch MailService.Protocol { case "smtp": MailService.SMTPPort = "25" case "smtps": MailService.SMTPPort = "465" - case "smtp+startls": + case "smtp+starttls": MailService.SMTPPort = "587" } } + // Infer Protocol if MailService.Protocol == "" { if strings.ContainsAny(MailService.SMTPAddr, "/\\") { MailService.Protocol = "smtp+unix" @@ -131,60 +175,38 @@ func newMailService() { case "465": MailService.Protocol = "smtps" case "587": - MailService.Protocol = "smtp+startls" + MailService.Protocol = "smtp+starttls" default: log.Error("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = %q, assume using smtps", MailService.SMTPPort) MailService.Protocol = "smtps" + if MailService.SMTPPort == "" { + MailService.SMTPPort = "465" + } } } } // we want to warn if users use SMTP on a non-local IP; // we might as well take the opportunity to check that it has an IP at all - ips := tryResolveAddr(MailService.SMTPAddr) - if MailService.Protocol == "smtp" { - for _, ip := range ips { - if !ip.IsLoopback() { - log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended") - break + // This check is not needed for sendmail + switch MailService.Protocol { + case "sendmail": + var err error + MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String()) + if err != nil { + log.Error("Failed to parse Sendmail args: '%s' with error %v", sec.Key("SENDMAIL_ARGS").String(), err) + } + case "smtp", "smtps", "smtp+starttls", "smtp+unix": + ips := tryResolveAddr(MailService.SMTPAddr) + if MailService.Protocol == "smtp" { + for _, ip := range ips { + if !ip.IsLoopback() { + log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended") + break + } } } - } - - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting("mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO") - if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") { - MailService.EnableHelo = !sec.Key("DISABLE_HELO").MustBool() - } - - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting("mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT") - if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") { - MailService.ForceTrustServerCert = sec.Key("SKIP_VERIFY").MustBool() - } - - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting("mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT") - if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") { - MailService.UseClientCert = sec.Key("USE_CLIENT_CERT").MustBool() - } - - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting("mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE") - if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") { - MailService.ClientCertFile = sec.Key("CERT_FILE").String() - } - - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting("mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE") - if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") { - MailService.ClientKeyFile = sec.Key("KEY_FILE").String() - } - - // FIXME: DEPRECATED to be removed in v1.19.0 - deprecatedSetting("mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT") - if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") { - MailService.SendAsPlainText = !sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false) + case "dummy": // just mention and do nothing } if MailService.From != "" { @@ -213,14 +235,6 @@ func newMailService() { MailService.EnvelopeFrom = parsed.Address } - if MailService.Protocol == "sendmail" { - var err error - MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String()) - if err != nil { - log.Error("Failed to parse Sendmail args: %s with error %v", CustomConf, err) - } - } - log.Info("Mail Service Enabled") } diff --git a/modules/setting/mailer_test.go b/modules/setting/mailer_test.go new file mode 100644 index 0000000000..0fc9b0e73f --- /dev/null +++ b/modules/setting/mailer_test.go @@ -0,0 +1,43 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" + ini "gopkg.in/ini.v1" +) + +func TestParseMailerConfig(t *testing.T) { + iniFile := ini.Empty() + kases := map[string]*Mailer{ + "smtp.mydomain.com": { + SMTPAddr: "smtp.mydomain.com", + SMTPPort: "465", + }, + "smtp.mydomain.com:123": { + SMTPAddr: "smtp.mydomain.com", + SMTPPort: "123", + }, + ":123": { + SMTPAddr: "127.0.0.1", + SMTPPort: "123", + }, + } + for host, kase := range kases { + t.Run(host, func(t *testing.T) { + iniFile.DeleteSection("mailer") + sec := iniFile.Section("mailer") + sec.NewKey("ENABLED", "true") + sec.NewKey("HOST", host) + + // Check mailer setting + parseMailerConfig(iniFile) + + assert.EqualValues(t, kase.SMTPAddr, MailService.SMTPAddr) + assert.EqualValues(t, kase.SMTPPort, MailService.SMTPPort) + }) + } +} diff --git a/modules/setting/picture.go b/modules/setting/picture.go index af9041ade3..4b61d51ac5 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -69,7 +69,7 @@ func newPictureService() { } func GetDefaultDisableGravatar() bool { - return !OfflineMode + return OfflineMode } func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool { diff --git a/modules/setting/repository.go b/modules/setting/repository.go index d0406dbf90..b6aa89909a 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -83,6 +83,7 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool + TestConflictingPatchesWithGitApply bool } `ini:"repository.pull-request"` // Issue Setting @@ -205,6 +206,7 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool + TestConflictingPatchesWithGitApply bool }{ WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, // Same as GitHub. See @@ -219,6 +221,7 @@ var ( DefaultMergeMessageOfficialApproversOnly: true, PopulateSquashCommentWithCommitMessages: false, AddCoCommitterTrailers: true, + TestConflictingPatchesWithGitApply: true, }, // Issue settings @@ -273,7 +276,7 @@ func newRepository() { Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool() Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch) - RepoRootPath = sec.Key("ROOT").MustString(path.Join(AppDataPath, "gitea-repositories")) + RepoRootPath = sec.Key("ROOT").MustString(path.Join(AppDataPath, "forgejo-repositories")) forcePathSeparator(RepoRootPath) if !filepath.IsAbs(RepoRootPath) { RepoRootPath = filepath.Join(AppWorkPath, RepoRootPath) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index f93be2fbd1..e32e3fc679 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -21,7 +21,9 @@ import ( "text/template" "time" + "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/user" @@ -285,11 +287,11 @@ var ( ReactionMaxUserNum: 10, ThemeColorMetaTag: `#6cc644`, MaxDisplayFileSize: 8388608, - DefaultTheme: `auto`, - Themes: []string{`auto`, `gitea`, `arc-green`}, + DefaultTheme: `forgejo-auto`, + Themes: []string{`forgejo-auto`, `forgejo-light`, `forgejo-dark`, `auto`, `gitea`, `arc-green`}, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, - CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, - CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, + CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`, `forgejo`}, + CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:", "forgejo": ":forgejo:"}, Notification: struct { MinTimeout time.Duration TimeoutStep time.Duration @@ -332,9 +334,9 @@ var ( Description string Keywords string }{ - Author: "Gitea - Git with a cup of tea", - Description: "Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go", - Keywords: "go,git,self-hosted,gitea", + Author: "Forgejo – Beyond coding. We forge.", + Description: "Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job.", + Keywords: "git,forge,forgejo", }, } @@ -463,6 +465,13 @@ func getAppPath() (string, error) { appPath, err = exec.LookPath(os.Args[0]) } + if err != nil { + // FIXME: Once we switch to go 1.19 use !errors.Is(err, exec.ErrDot) + if !strings.Contains(err.Error(), "cannot run executable found relative to current directory") { + return "", err + } + appPath, err = filepath.Abs(os.Args[0]) + } if err != nil { return "", err } @@ -602,7 +611,7 @@ func LoadForTest(extraConfigs ...string) { func deprecatedSetting(oldSection, oldKey, newSection, newKey string) { if Cfg.Section(oldSection).HasKey(oldKey) { - log.Error("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be removed in v1.18.0", oldSection, oldKey, newSection, newKey) + log.Error("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be removed in v1.19.0", oldSection, oldKey, newSection, newKey) } } @@ -654,7 +663,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) { forcePathSeparator(LogRootPath) sec := Cfg.Section("server") - AppName = Cfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea") + AppName = Cfg.Section("").Key("APP_NAME").MustString("Forgejo: Beyond coding. We Forge.") Domain = sec.Key("DOMAIN").MustString("localhost") HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") @@ -936,7 +945,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) { if SecretKey == "" { // FIXME: https://github.com/go-gitea/gitea/issues/16832 // Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value - SecretKey = "!#@FDEWREWR&*(" // nolint:gosec + SecretKey = "!#@FDEWREWR&*(" //nolint:gosec } CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible") @@ -956,12 +965,24 @@ func loadFromConf(allowEmpty bool, extraConfig string) { DisableGitHooks = sec.Key("DISABLE_GIT_HOOKS").MustBool(true) DisableWebhooks = sec.Key("DISABLE_WEBHOOKS").MustBool(false) OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET").MustBool(true) - PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("pbkdf2") + + // Ensure that the provided default hash algorithm is a valid hash algorithm + var algorithm *hash.PasswordHashAlgorithm + PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(sec.Key("PASSWORD_HASH_ALGO").MustString("")) + if algorithm == nil { + log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString("")) + } + CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN") + if InstallLock && InternalToken == "" { + // if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate + // some users do cluster deployment, they still depend on this auto-generating behavior. + generateSaveInternalToken() + } cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",") if len(cfgdata) == 0 { @@ -1026,7 +1047,10 @@ func loadFromConf(allowEmpty bool, extraConfig string) { // The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches. // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. unsafeAllowRunAsRoot := Cfg.Section("").Key("I_AM_BEING_UNSAFE_RUNNING_AS_ROOT").MustBool(false) - RunMode = Cfg.Section("").Key("RUN_MODE").MustString("prod") + RunMode = os.Getenv("GITEA_RUN_MODE") + if RunMode == "" { + RunMode = Cfg.Section("").Key("RUN_MODE").MustString("prod") + } IsProd = strings.EqualFold(RunMode, "prod") // Does not check run user when the install lock is off. if InstallLock { @@ -1150,6 +1174,8 @@ func parseAuthorizedPrincipalsAllow(values []string) ([]string, bool) { return authorizedPrincipalsAllow, true } +// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set +// If the secret is loaded from uriKey (file), the file should be non-empty, to guarantee the behavior stable and clear. func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string { // don't allow setting both URI and verbatim string uri := sec.Key(uriKey).String() @@ -1173,7 +1199,15 @@ func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string { if err != nil { log.Fatal("Failed to read %s (%s): %v", uriKey, tempURI.RequestURI(), err) } - return strings.TrimSpace(string(buf)) + val := strings.TrimSpace(string(buf)) + if val == "" { + // The file shouldn't be empty, otherwise we can not know whether the user has ever set the KEY or KEY_URI + // For example: if INTERNAL_TOKEN_URI=file:///empty-file, + // Then if the token is re-generated during installation and saved to INTERNAL_TOKEN + // Then INTERNAL_TOKEN and INTERNAL_TOKEN_URI both exist, that's a fatal error (they shouldn't) + log.Fatal("Failed to read %s (%s): the file is empty", uriKey, tempURI.RequestURI()) + } + return val // only file URIs are allowed default: @@ -1182,6 +1216,19 @@ func loadSecret(sec *ini.Section, uriKey, verbatimKey string) string { } } +// generateSaveInternalToken generates and saves the internal token to app.ini +func generateSaveInternalToken() { + token, err := generate.NewInternalToken() + if err != nil { + log.Fatal("Error generate internal token: %v", err) + } + + InternalToken = token + CreateOrAppendToCustomConf("security.INTERNAL_TOKEN", func(cfg *ini.File) { + cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token) + }) +} + // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) @@ -1295,7 +1342,7 @@ func NewServices() { newCacheService() newSessionService() newCORSService() - newMailService() + parseMailerConfig(Cfg) newRegisterMailService() newNotifyMailService() newProxyService() @@ -1312,5 +1359,5 @@ func NewServices() { // NewServicesForInstall initializes the services for install func NewServicesForInstall() { newService() - newMailService() + parseMailerConfig(Cfg) } diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go index 0bfd7dcb4d..be78066a3d 100644 --- a/modules/setting/webhook.go +++ b/modules/setting/webhook.go @@ -36,7 +36,7 @@ func newWebhookService() { Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5) Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool() Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString("") - Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"} + Webhook.Types = []string{"forgejo", "gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"} Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10) Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("") if Webhook.ProxyURL != "" { diff --git a/modules/sitemap/sitemap.go b/modules/sitemap/sitemap.go index 14953765ab..4654c5fa38 100644 --- a/modules/sitemap/sitemap.go +++ b/modules/sitemap/sitemap.go @@ -12,48 +12,62 @@ import ( "time" ) -// sitemapFileLimit contains the maximum size of a sitemap file -const sitemapFileLimit = 50 * 1024 * 1024 +const ( + sitemapFileLimit = 50 * 1024 * 1024 // the maximum size of a sitemap file + urlsLimit = 50000 -// Url represents a single sitemap entry + schemaURL = "http://www.sitemaps.org/schemas/sitemap/0.9" + urlsetName = "urlset" + sitemapindexName = "sitemapindex" +) + +// URL represents a single sitemap entry type URL struct { URL string `xml:"loc"` LastMod *time.Time `xml:"lastmod,omitempty"` } -// SitemapUrl represents a sitemap +// Sitemap represents a sitemap type Sitemap struct { XMLName xml.Name Namespace string `xml:"xmlns,attr"` - URLs []URL `xml:"url"` + URLs []URL `xml:"url"` + Sitemaps []URL `xml:"sitemap"` } // NewSitemap creates a sitemap func NewSitemap() *Sitemap { return &Sitemap{ - XMLName: xml.Name{Local: "urlset"}, - Namespace: "http://www.sitemaps.org/schemas/sitemap/0.9", + XMLName: xml.Name{Local: urlsetName}, + Namespace: schemaURL, } } -// NewSitemap creates a sitemap index. +// NewSitemapIndex creates a sitemap index. func NewSitemapIndex() *Sitemap { return &Sitemap{ - XMLName: xml.Name{Local: "sitemapindex"}, - Namespace: "http://www.sitemaps.org/schemas/sitemap/0.9", + XMLName: xml.Name{Local: sitemapindexName}, + Namespace: schemaURL, } } // Add adds a URL to the sitemap func (s *Sitemap) Add(u URL) { - s.URLs = append(s.URLs, u) + if s.XMLName.Local == sitemapindexName { + s.Sitemaps = append(s.Sitemaps, u) + } else { + s.URLs = append(s.URLs, u) + } } -// Write writes the sitemap to a response +// WriteTo writes the sitemap to a response func (s *Sitemap) WriteTo(w io.Writer) (int64, error) { - if len(s.URLs) > 50000 { - return 0, fmt.Errorf("The sitemap contains too many URLs: %d", len(s.URLs)) + if l := len(s.URLs); l > urlsLimit { + return 0, fmt.Errorf("The sitemap contains %d URLs, but only %d are allowed", l, urlsLimit) + } + if l := len(s.Sitemaps); l > urlsLimit { + return 0, fmt.Errorf("The sitemap contains %d sub-sitemaps, but only %d are allowed", l, urlsLimit) } buf := bytes.NewBufferString(xml.Header) if err := xml.NewEncoder(buf).Encode(s); err != nil { @@ -63,7 +77,7 @@ func (s *Sitemap) WriteTo(w io.Writer) (int64, error) { return 0, err } if buf.Len() > sitemapFileLimit { - return 0, fmt.Errorf("The sitemap is too big: %d", buf.Len()) + return 0, fmt.Errorf("The sitemap has %d bytes, but only %d are allowed", buf.Len(), sitemapFileLimit) } return buf.WriteTo(w) } diff --git a/modules/sitemap/sitemap_test.go b/modules/sitemap/sitemap_test.go index 63007b8479..c7c9cbcd9f 100644 --- a/modules/sitemap/sitemap_test.go +++ b/modules/sitemap/sitemap_test.go @@ -7,7 +7,6 @@ package sitemap import ( "bytes" "encoding/xml" - "fmt" "strings" "testing" "time" @@ -15,63 +14,154 @@ import ( "github.com/stretchr/testify/assert" ) -func TestOk(t *testing.T) { - testReal := func(s *Sitemap, name string, urls []URL, expected string) { - for _, url := range urls { - s.Add(url) - } - buf := &bytes.Buffer{} - _, err := s.WriteTo(buf) - assert.NoError(t, nil, err) - assert.Equal(t, xml.Header+"<"+name+" xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"+expected+"\n", buf.String()) - } - test := func(urls []URL, expected string) { - testReal(NewSitemap(), "urlset", urls, expected) - testReal(NewSitemapIndex(), "sitemapindex", urls, expected) - } - +func TestNewSitemap(t *testing.T) { ts := time.Unix(1651322008, 0).UTC() - test( - []URL{}, - "", - ) - test( - []URL{ - {URL: "https://gitea.io/test1", LastMod: &ts}, + tests := []struct { + name string + urls []URL + want string + wantErr string + }{ + { + name: "empty", + urls: []URL{}, + want: xml.Header + `` + + "" + + "\n", }, - "https://gitea.io/test12022-04-30T12:33:28Z", - ) - test( - []URL{ - {URL: "https://gitea.io/test2", LastMod: nil}, + { + name: "regular", + urls: []URL{ + {URL: "https://gitea.io/test1", LastMod: &ts}, + }, + want: xml.Header + `` + + "https://gitea.io/test12022-04-30T12:33:28Z" + + "\n", }, - "https://gitea.io/test2", - ) - test( - []URL{ - {URL: "https://gitea.io/test1", LastMod: &ts}, - {URL: "https://gitea.io/test2", LastMod: nil}, + { + name: "without lastmod", + urls: []URL{ + {URL: "https://gitea.io/test1"}, + }, + want: xml.Header + `` + + "https://gitea.io/test1" + + "\n", + }, + { + name: "multiple", + urls: []URL{ + {URL: "https://gitea.io/test1", LastMod: &ts}, + {URL: "https://gitea.io/test2", LastMod: nil}, + }, + want: xml.Header + `` + + "https://gitea.io/test12022-04-30T12:33:28Z" + + "https://gitea.io/test2" + + "\n", + }, + { + name: "too many urls", + urls: make([]URL, 50001), + wantErr: "The sitemap contains 50001 URLs, but only 50000 are allowed", + }, + { + name: "too big file", + urls: []URL{ + {URL: strings.Repeat("b", 50*1024*1024+1)}, + }, + wantErr: "The sitemap has 52428932 bytes, but only 52428800 are allowed", }, - "https://gitea.io/test12022-04-30T12:33:28Z"+ - "https://gitea.io/test2", - ) -} - -func TestTooManyURLs(t *testing.T) { - s := NewSitemap() - for i := 0; i < 50001; i++ { - s.Add(URL{URL: fmt.Sprintf("https://gitea.io/test%d", i)}) } - buf := &bytes.Buffer{} - _, err := s.WriteTo(buf) - assert.EqualError(t, err, "The sitemap contains too many URLs: 50001") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := NewSitemap() + for _, url := range tt.urls { + s.Add(url) + } + buf := &bytes.Buffer{} + _, err := s.WriteTo(buf) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + assert.Equalf(t, tt.want, buf.String(), "NewSitemap()") + } + }) + } } -func TestSitemapTooBig(t *testing.T) { - s := NewSitemap() - s.Add(URL{URL: strings.Repeat("b", sitemapFileLimit)}) - buf := &bytes.Buffer{} - _, err := s.WriteTo(buf) - assert.EqualError(t, err, "The sitemap is too big: 52428931") +func TestNewSitemapIndex(t *testing.T) { + ts := time.Unix(1651322008, 0).UTC() + + tests := []struct { + name string + urls []URL + want string + wantErr string + }{ + { + name: "empty", + urls: []URL{}, + want: xml.Header + `` + + "" + + "\n", + }, + { + name: "regular", + urls: []URL{ + {URL: "https://gitea.io/test1", LastMod: &ts}, + }, + want: xml.Header + `` + + "https://gitea.io/test12022-04-30T12:33:28Z" + + "\n", + }, + { + name: "without lastmod", + urls: []URL{ + {URL: "https://gitea.io/test1"}, + }, + want: xml.Header + `` + + "https://gitea.io/test1" + + "\n", + }, + { + name: "multiple", + urls: []URL{ + {URL: "https://gitea.io/test1", LastMod: &ts}, + {URL: "https://gitea.io/test2", LastMod: nil}, + }, + want: xml.Header + `` + + "https://gitea.io/test12022-04-30T12:33:28Z" + + "https://gitea.io/test2" + + "\n", + }, + { + name: "too many sitemaps", + urls: make([]URL, 50001), + wantErr: "The sitemap contains 50001 sub-sitemaps, but only 50000 are allowed", + }, + { + name: "too big file", + urls: []URL{ + {URL: strings.Repeat("b", 50*1024*1024+1)}, + }, + wantErr: "The sitemap has 52428952 bytes, but only 52428800 are allowed", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := NewSitemapIndex() + for _, url := range tt.urls { + s.Add(url) + } + buf := &bytes.Buffer{} + _, err := s.WriteTo(buf) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + assert.Equalf(t, tt.want, buf.String(), "NewSitemapIndex()") + } + }) + } } diff --git a/modules/storage/local.go b/modules/storage/local.go index 5d5b06b648..a208ba8e10 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -103,7 +103,8 @@ func (l *LocalStorage) Save(path string, r io.Reader, size int64) (int64, error) return 0, err } // Golang's tmp file (os.CreateTemp) always have 0o600 mode, so we need to change the file to follow the umask (as what Create/MkDir does) - if err := util.ApplyUmask(p, os.ModePerm); err != nil { + // but we don't want to make these files executable - so ensure that we mask out the executable bits + if err := util.ApplyUmask(p, os.ModePerm&0o666); err != nil { return 0, err } diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 8321a15a8f..503f983151 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -40,7 +40,7 @@ type CreateHookOptionConfig map[string]string // CreateHookOption options when create a hook type CreateHookOption struct { // required: true - // enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist + // enum: forgejo,dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist Type string `json:"type" binding:"Required"` // required: true Config CreateHookOptionConfig `json:"config" binding:"Required"` diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 27ec81f728..45c3f6294a 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -5,8 +5,12 @@ package structs import ( - "path/filepath" + "fmt" + "path" + "strings" "time" + + "gopkg.in/yaml.v3" ) // StateType issue state type @@ -143,14 +147,47 @@ type IssueFormField struct { // IssueTemplate represents an issue template for a repository // swagger:model type IssueTemplate struct { - Name string `json:"name" yaml:"name"` - Title string `json:"title" yaml:"title"` - About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible - Labels []string `json:"labels" yaml:"labels"` - Ref string `json:"ref" yaml:"ref"` - Content string `json:"content" yaml:"-"` - Fields []*IssueFormField `json:"body" yaml:"body"` - FileName string `json:"file_name" yaml:"-"` + Name string `json:"name" yaml:"name"` + Title string `json:"title" yaml:"title"` + About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible + Labels IssueTemplateLabels `json:"labels" yaml:"labels"` + Ref string `json:"ref" yaml:"ref"` + Content string `json:"content" yaml:"-"` + Fields []*IssueFormField `json:"body" yaml:"body"` + FileName string `json:"file_name" yaml:"-"` +} + +type IssueTemplateLabels []string + +func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error { + var labels []string + if value.IsZero() { + *l = labels + return nil + } + switch value.Kind { + case yaml.ScalarNode: + str := "" + err := value.Decode(&str) + if err != nil { + return err + } + for _, v := range strings.Split(str, ",") { + if v = strings.TrimSpace(v); v == "" { + continue + } + labels = append(labels, v) + } + *l = labels + return nil + case yaml.SequenceNode: + if err := value.Decode(&labels); err != nil { + return err + } + *l = labels + return nil + } + return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag()) } // IssueTemplateType defines issue template type @@ -163,14 +200,14 @@ const ( // Type returns the type of IssueTemplate, can be "md", "yaml" or empty for known func (it IssueTemplate) Type() IssueTemplateType { - if it.Name == "config.yaml" || it.Name == "config.yml" { + if base := path.Base(it.FileName); base == "config.yaml" || base == "config.yml" { // ignore config.yaml which is a special configuration file return "" } - if ext := filepath.Ext(it.FileName); ext == ".md" { + if ext := path.Ext(it.FileName); ext == ".md" { return IssueTemplateTypeMarkdown } else if ext == ".yaml" || ext == ".yml" { - return "yaml" + return IssueTemplateTypeYaml } - return IssueTemplateTypeYaml + return "" } diff --git a/modules/structs/issue_test.go b/modules/structs/issue_test.go new file mode 100644 index 0000000000..72b40f7cf2 --- /dev/null +++ b/modules/structs/issue_test.go @@ -0,0 +1,106 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package structs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestIssueTemplate_Type(t *testing.T) { + tests := []struct { + fileName string + want IssueTemplateType + }{ + { + fileName: ".gitea/ISSUE_TEMPLATE/bug_report.yaml", + want: IssueTemplateTypeYaml, + }, + { + fileName: ".gitea/ISSUE_TEMPLATE/bug_report.md", + want: IssueTemplateTypeMarkdown, + }, + { + fileName: ".gitea/ISSUE_TEMPLATE/bug_report.txt", + want: "", + }, + { + fileName: ".gitea/ISSUE_TEMPLATE/config.yaml", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.fileName, func(t *testing.T) { + it := IssueTemplate{ + FileName: tt.fileName, + } + assert.Equal(t, tt.want, it.Type()) + }) + } +} + +func TestIssueTemplateLabels_UnmarshalYAML(t *testing.T) { + tests := []struct { + name string + content string + tmpl *IssueTemplate + want *IssueTemplate + wantErr string + }{ + { + name: "array", + content: `labels: ["a", "b", "c"]`, + tmpl: &IssueTemplate{ + Labels: []string{"should_be_overwrote"}, + }, + want: &IssueTemplate{ + Labels: []string{"a", "b", "c"}, + }, + }, + { + name: "string", + content: `labels: "a,b,c"`, + tmpl: &IssueTemplate{ + Labels: []string{"should_be_overwrote"}, + }, + want: &IssueTemplate{ + Labels: []string{"a", "b", "c"}, + }, + }, + { + name: "empty", + content: `labels:`, + tmpl: &IssueTemplate{ + Labels: []string{"should_be_overwrote"}, + }, + want: &IssueTemplate{ + Labels: nil, + }, + }, + { + name: "error", + content: ` +labels: + a: aa + b: bb +`, + tmpl: &IssueTemplate{}, + wantErr: "line 3: cannot unmarshal !!map into IssueTemplateLabels", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := yaml.Unmarshal([]byte(tt.content), tt.tmpl) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, tt.tmpl) + } + }) + } +} diff --git a/modules/structs/mirror.go b/modules/structs/mirror.go index 8e8a8a2705..cb6d567056 100644 --- a/modules/structs/mirror.go +++ b/modules/structs/mirror.go @@ -10,6 +10,7 @@ type CreatePushMirrorOption struct { RemoteUsername string `json:"remote_username"` RemotePassword string `json:"remote_password"` Interval string `json:"interval"` + SyncOnCommit bool `json:"sync_on_commit"` } // PushMirror represents information of a push mirror @@ -22,4 +23,5 @@ type PushMirror struct { LastUpdateUnix string `json:"last_update"` LastError string `json:"last_error"` Interval string `json:"interval"` + SyncOnCommit bool `json:"sync_on_commit"` } diff --git a/modules/system/item_runtime.go b/modules/system/item_runtime.go index ef758a5675..e022a0daad 100644 --- a/modules/system/item_runtime.go +++ b/modules/system/item_runtime.go @@ -6,7 +6,8 @@ package system // RuntimeState contains app state for runtime, and we can save remote version for update checker here in future type RuntimeState struct { - LastAppPath string `json:"last_app_path"` + LastAppPath string `json:"last_app_path"` + LastCustomConf string `json:"last_custom_conf"` } // Name returns the item name diff --git a/modules/system/setting.go b/modules/system/setting.go deleted file mode 100644 index aebf24a501..0000000000 --- a/modules/system/setting.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package system - -import ( - "strconv" - - "code.gitea.io/gitea/models/system" - "code.gitea.io/gitea/modules/cache" -) - -func genKey(key string) string { - return "system.setting." + key -} - -// GetSetting returns the setting value via the key -func GetSetting(key string) (string, error) { - return cache.GetString(genKey(key), func() (string, error) { - res, err := system.GetSetting(key) - if err != nil { - return "", err - } - return res.SettingValue, nil - }) -} - -// GetSettingBool return bool value of setting, -// none existing keys and errors are ignored and result in false -func GetSettingBool(key string) bool { - s, _ := GetSetting(key) - b, _ := strconv.ParseBool(s) - return b -} - -// SetSetting sets the setting value -func SetSetting(key, value string, version int) error { - cache.Remove(genKey(key)) - - return system.SetSetting(&system.Setting{ - SettingKey: key, - SettingValue: value, - Version: version, - }) -} diff --git a/modules/system/user_setting.go b/modules/system/user_setting.go deleted file mode 100644 index eaf146c08d..0000000000 --- a/modules/system/user_setting.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package system - -import ( - "fmt" - - "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/cache" -) - -func genUserKey(userID int64, key string) string { - return fmt.Sprintf("user_%d.setting.%s", userID, key) -} - -// GetUserSetting returns the user setting value via the key -func GetUserSetting(userID int64, key string) (string, error) { - return cache.GetString(genUserKey(userID, key), func() (string, error) { - res, err := user.GetSetting(userID, key) - if err != nil { - return "", err - } - return res.SettingValue, nil - }) -} - -// SetUserSetting sets the user setting value -func SetUserSetting(userID int64, key, value string) error { - cache.Remove(genUserKey(userID, key)) - - return user.SetUserSetting(userID, key, value) -} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index a723291440..9aca94971e 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -42,7 +42,6 @@ import ( "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" - system_module "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/gitdiff" @@ -87,7 +86,7 @@ func NewFuncMap() []template.FuncMap { return setting.AssetVersion }, "DisableGravatar": func() bool { - return system_module.GetSettingBool(system_model.KeyPictureDisableGravatar) + return system_model.GetSettingBool(system_model.KeyPictureDisableGravatar) }, "DefaultShowFullName": func() bool { return setting.UI.DefaultShowFullName @@ -647,7 +646,7 @@ func SVG(icon string, others ...interface{}) template.HTML { // Avatar renders user avatars. args: user, size (int), class (string) func Avatar(item interface{}, others ...interface{}) template.HTML { - size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar vm", others...) + size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) switch t := item.(type) { case *user_model.User: @@ -678,7 +677,7 @@ func AvatarByAction(action *activities_model.Action, others ...interface{}) temp // RepoAvatar renders repo avatars. args: repo, size(int), class (string) func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML { - size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar", others...) + size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) src := repo.RelAvatarLink() if src != "" { @@ -689,7 +688,7 @@ func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTM // AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string) func AvatarByEmail(email, name string, others ...interface{}) template.HTML { - size, class := parseOthers(avatars.DefaultAvatarPixelSize, "ui avatar", others...) + size, class := parseOthers(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...) src := avatars.GenerateEmailAvatarFastLink(email, size*setting.Avatar.RenderedSizeFactor) if src != "" { diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 81ea660161..c3d747d12d 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -76,8 +76,15 @@ func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) { compilingTemplates = false if !setting.IsProd { watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{ - PathsCallback: walkTemplateFiles, - BetweenCallback: renderer.CompileTemplates, + PathsCallback: walkTemplateFiles, + BetweenCallback: func() { + defer func() { + if err := recover(); err != nil { + log.Error("PANIC: %v\n%s", err, log.Stack(2)) + } + }() + renderer.CompileTemplates() + }, }) } return context.WithValue(ctx, rendererKey, renderer), renderer diff --git a/modules/timeutil/timestamp.go b/modules/timeutil/timestamp.go index 40fcb8603f..ad074edd45 100644 --- a/modules/timeutil/timestamp.go +++ b/modules/timeutil/timestamp.go @@ -13,8 +13,13 @@ import ( // TimeStamp defines a timestamp type TimeStamp int64 -// mock is NOT concurrency-safe!! -var mock time.Time +var ( + // mock is NOT concurrency-safe!! + mock time.Time + + // Used for IsZero, to check if timestamp is the zero time instant. + timeZeroUnix = time.Time{}.Unix() +) // Set sets the time to a mocked time.Time func Set(now time.Time) { @@ -103,5 +108,5 @@ func (ts TimeStamp) FormatDate() string { // IsZero is zero time func (ts TimeStamp) IsZero() bool { - return ts.AsTimeInLocation(time.Local).IsZero() + return int64(ts) == 0 || int64(ts) == timeZeroUnix } diff --git a/modules/updatechecker/update_checker.go b/modules/updatechecker/update_checker.go index 816fb3764c..1fc99ddc60 100644 --- a/modules/updatechecker/update_checker.go +++ b/modules/updatechecker/update_checker.go @@ -5,8 +5,11 @@ package updatechecker import ( + "errors" "io" + "net" "net/http" + "strings" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/proxy" @@ -27,7 +30,51 @@ func (r *CheckerState) Name() string { } // GiteaUpdateChecker returns error when new version of Gitea is available -func GiteaUpdateChecker(httpEndpoint string) error { +func GiteaUpdateChecker(httpEndpoint, domainEndpoint string) error { + var version string + var err error + if domainEndpoint != "" { + version, err = getVersionDNS(domainEndpoint) + } else { + version, err = getVersionHTTP(httpEndpoint) + } + + if err != nil { + return err + } + + return UpdateRemoteVersion(version) +} + +// getVersionDNS will request the TXT records for the domain. If a record starts +// with "forgejo_versions=" everything after that will be used as the latest +// version available. +func getVersionDNS(domainEndpoint string) (version string, err error) { + records, err := net.LookupTXT(domainEndpoint) + if err != nil { + return "", err + } + + if len(records) == 0 { + return "", errors.New("no TXT records were found") + } + + for _, record := range records { + if strings.HasPrefix(record, "forgejo_versions=") { + // Get all supported versions, separated by a comma. + supportedVersions := strings.Split(strings.TrimPrefix(record, "forgejo_versions="), ",") + // For now always return the latest supported version. + return supportedVersions[len(supportedVersions)-1], nil + } + } + + return "", errors.New("there is no TXT record with a valid value") +} + +// getVersionHTTP will make an HTTP request to the endpoint, and the returned +// content is JSON. The "latest.version" path's value will be used as the latest +// version available. +func getVersionHTTP(httpEndpoint string) (version string, err error) { httpClient := &http.Client{ Transport: &http.Transport{ Proxy: proxy.Proxy(), @@ -36,16 +83,16 @@ func GiteaUpdateChecker(httpEndpoint string) error { req, err := http.NewRequest("GET", httpEndpoint, nil) if err != nil { - return err + return "", err } resp, err := httpClient.Do(req) if err != nil { - return err + return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return err + return "", err } type respType struct { @@ -56,10 +103,9 @@ func GiteaUpdateChecker(httpEndpoint string) error { respData := respType{} err = json.Unmarshal(body, &respData) if err != nil { - return err + return "", err } - - return UpdateRemoteVersion(respData.Latest.Version) + return respData.Latest.Version, nil } // UpdateRemoteVersion updates the latest available version of Gitea diff --git a/modules/updatechecker/update_checker_test.go b/modules/updatechecker/update_checker_test.go new file mode 100644 index 0000000000..301afd95e4 --- /dev/null +++ b/modules/updatechecker/update_checker_test.go @@ -0,0 +1,16 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package updatechecker + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDNSUpdate(t *testing.T) { + version, err := getVersionDNS("release.forgejo.org") + assert.NoError(t, err) + assert.NotEmpty(t, version) +} diff --git a/modules/util/io.go b/modules/util/io.go index d765e27733..f3a24736a8 100644 --- a/modules/util/io.go +++ b/modules/util/io.go @@ -5,6 +5,7 @@ package util import ( + "errors" "io" ) @@ -18,3 +19,24 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { } return n, err } + +// ErrNotEmpty is an error reported when there is a non-empty reader +var ErrNotEmpty = errors.New("not-empty") + +// IsEmptyReader reads a reader and ensures it is empty +func IsEmptyReader(r io.Reader) (err error) { + var buf [1]byte + + for { + n, err := r.Read(buf[:]) + if err != nil { + if err == io.EOF { + return nil + } + return err + } + if n > 0 { + return ErrNotEmpty + } + } +} diff --git a/modules/util/path.go b/modules/util/path.go index 3d4ddec21c..ac2b82acfe 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -23,20 +23,6 @@ func EnsureAbsolutePath(path, absoluteBase string) string { return filepath.Join(absoluteBase, path) } -const notRegularFileMode os.FileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular - -// GetDirectorySize returns the disk consumption for a given path -func GetDirectorySize(path string) (int64, error) { - var size int64 - err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { - if info != nil && (info.Mode()¬RegularFileMode) == 0 { - size += info.Size() - } - return err - }) - return size, err -} - // IsDir returns true if given path is a directory, // or returns false when it's a file or does not exist. func IsDir(dir string) (bool, error) { diff --git a/options/locale/locale_bg-BG.ini b/options/locales/gitea_bg-BG.ini similarity index 100% rename from options/locale/locale_bg-BG.ini rename to options/locales/gitea_bg-BG.ini diff --git a/options/locale/locale_cs-CZ.ini b/options/locales/gitea_cs-CZ.ini similarity index 100% rename from options/locale/locale_cs-CZ.ini rename to options/locales/gitea_cs-CZ.ini diff --git a/options/locale/locale_de-DE.ini b/options/locales/gitea_de-DE.ini similarity index 100% rename from options/locale/locale_de-DE.ini rename to options/locales/gitea_de-DE.ini diff --git a/options/locale/locale_el-GR.ini b/options/locales/gitea_el-GR.ini similarity index 100% rename from options/locale/locale_el-GR.ini rename to options/locales/gitea_el-GR.ini diff --git a/options/locale/locale_en-US.ini b/options/locales/gitea_en-US.ini similarity index 99% rename from options/locale/locale_en-US.ini rename to options/locales/gitea_en-US.ini index 1566dfc97d..543b1a6b6e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locales/gitea_en-US.ini @@ -106,6 +106,12 @@ never = Never rss_feed = RSS Feed +[aria] +navbar = Navigation Bar +footer = Footer +footer.software = About Software +footer.links = Links + [filter] string.asc = A - Z string.desc = Z - A @@ -497,6 +503,7 @@ team_not_exist = The team does not exist. last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization. cannot_add_org_to_team = An organization cannot be added as a team member. duplicate_invite_to_team = The user was already invited as a team member. +organization_leave_success = You have successfully left the organization %s. invalid_ssh_key = Can not verify your SSH key: %s invalid_gpg_key = Can not verify your GPG key: %s @@ -1111,6 +1118,7 @@ editor.commit_directly_to_this_branch = Commit directly to the %[3]s pulls.merged_by = by %[3]s was merged %[1]s pulls.merged_by_fake = by %[2]s was merged %[1]s diff --git a/options/locale/locale_es-ES.ini b/options/locales/gitea_es-ES.ini similarity index 100% rename from options/locale/locale_es-ES.ini rename to options/locales/gitea_es-ES.ini diff --git a/options/locale/locale_fa-IR.ini b/options/locales/gitea_fa-IR.ini similarity index 100% rename from options/locale/locale_fa-IR.ini rename to options/locales/gitea_fa-IR.ini diff --git a/options/locale/locale_fi-FI.ini b/options/locales/gitea_fi-FI.ini similarity index 100% rename from options/locale/locale_fi-FI.ini rename to options/locales/gitea_fi-FI.ini diff --git a/options/locale/locale_fr-FR.ini b/options/locales/gitea_fr-FR.ini similarity index 100% rename from options/locale/locale_fr-FR.ini rename to options/locales/gitea_fr-FR.ini diff --git a/options/locale/locale_hu-HU.ini b/options/locales/gitea_hu-HU.ini similarity index 100% rename from options/locale/locale_hu-HU.ini rename to options/locales/gitea_hu-HU.ini diff --git a/options/locale/locale_id-ID.ini b/options/locales/gitea_id-ID.ini similarity index 100% rename from options/locale/locale_id-ID.ini rename to options/locales/gitea_id-ID.ini diff --git a/options/locale/locale_is-IS.ini b/options/locales/gitea_is-IS.ini similarity index 100% rename from options/locale/locale_is-IS.ini rename to options/locales/gitea_is-IS.ini diff --git a/options/locale/locale_it-IT.ini b/options/locales/gitea_it-IT.ini similarity index 100% rename from options/locale/locale_it-IT.ini rename to options/locales/gitea_it-IT.ini diff --git a/options/locale/locale_ja-JP.ini b/options/locales/gitea_ja-JP.ini similarity index 100% rename from options/locale/locale_ja-JP.ini rename to options/locales/gitea_ja-JP.ini diff --git a/options/locale/locale_ko-KR.ini b/options/locales/gitea_ko-KR.ini similarity index 100% rename from options/locale/locale_ko-KR.ini rename to options/locales/gitea_ko-KR.ini diff --git a/options/locale/locale_lv-LV.ini b/options/locales/gitea_lv-LV.ini similarity index 100% rename from options/locale/locale_lv-LV.ini rename to options/locales/gitea_lv-LV.ini diff --git a/options/locale/locale_ml-IN.ini b/options/locales/gitea_ml-IN.ini similarity index 100% rename from options/locale/locale_ml-IN.ini rename to options/locales/gitea_ml-IN.ini diff --git a/options/locale/locale_nl-NL.ini b/options/locales/gitea_nl-NL.ini similarity index 100% rename from options/locale/locale_nl-NL.ini rename to options/locales/gitea_nl-NL.ini diff --git a/options/locale/locale_pl-PL.ini b/options/locales/gitea_pl-PL.ini similarity index 100% rename from options/locale/locale_pl-PL.ini rename to options/locales/gitea_pl-PL.ini diff --git a/options/locale/locale_pt-BR.ini b/options/locales/gitea_pt-BR.ini similarity index 100% rename from options/locale/locale_pt-BR.ini rename to options/locales/gitea_pt-BR.ini diff --git a/options/locale/locale_pt-PT.ini b/options/locales/gitea_pt-PT.ini similarity index 100% rename from options/locale/locale_pt-PT.ini rename to options/locales/gitea_pt-PT.ini diff --git a/options/locale/locale_ru-RU.ini b/options/locales/gitea_ru-RU.ini similarity index 100% rename from options/locale/locale_ru-RU.ini rename to options/locales/gitea_ru-RU.ini diff --git a/options/locale/locale_si-LK.ini b/options/locales/gitea_si-LK.ini similarity index 100% rename from options/locale/locale_si-LK.ini rename to options/locales/gitea_si-LK.ini diff --git a/options/locale/locale_sk-SK.ini b/options/locales/gitea_sk-SK.ini similarity index 100% rename from options/locale/locale_sk-SK.ini rename to options/locales/gitea_sk-SK.ini diff --git a/options/locale/locale_sv-SE.ini b/options/locales/gitea_sv-SE.ini similarity index 100% rename from options/locale/locale_sv-SE.ini rename to options/locales/gitea_sv-SE.ini diff --git a/options/locale/locale_tr-TR.ini b/options/locales/gitea_tr-TR.ini similarity index 100% rename from options/locale/locale_tr-TR.ini rename to options/locales/gitea_tr-TR.ini diff --git a/options/locale/locale_uk-UA.ini b/options/locales/gitea_uk-UA.ini similarity index 100% rename from options/locale/locale_uk-UA.ini rename to options/locales/gitea_uk-UA.ini diff --git a/options/locale/locale_zh-CN.ini b/options/locales/gitea_zh-CN.ini similarity index 100% rename from options/locale/locale_zh-CN.ini rename to options/locales/gitea_zh-CN.ini diff --git a/options/locale/locale_zh-HK.ini b/options/locales/gitea_zh-HK.ini similarity index 100% rename from options/locale/locale_zh-HK.ini rename to options/locales/gitea_zh-HK.ini diff --git a/options/locale/locale_zh-TW.ini b/options/locales/gitea_zh-TW.ini similarity index 100% rename from options/locale/locale_zh-TW.ini rename to options/locales/gitea_zh-TW.ini diff --git a/public/forgejo/api.v1.yml b/public/forgejo/api.v1.yml new file mode 100644 index 0000000000..903dd659d0 --- /dev/null +++ b/public/forgejo/api.v1.yml @@ -0,0 +1,40 @@ +openapi: 3.0.0 +info: + title: Forgejo API + description: |- + Forgejo REST API + + contact: + email: contact@forgejo.org + license: + name: MIT + url: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/LICENSE + version: 1.0.0 +externalDocs: + description: Find out more about Forgejo + url: http://forgejo.org +servers: + - url: /api/forgejo/v1 +paths: + /version: + get: + summary: API version + description: Semantic version of the Forgejo API + operationId: getVersion + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Version' +components: + schemas: + Version: + type: object + properties: + number: + type: string + diff --git a/public/img/apple-touch-icon.png b/public/img/apple-touch-icon.png index 0c803d35dc..1f6c1544f8 100644 Binary files a/public/img/apple-touch-icon.png and b/public/img/apple-touch-icon.png differ diff --git a/public/img/avatar_default.png b/public/img/avatar_default.png index 129967112d..f335e51dad 100644 Binary files a/public/img/avatar_default.png and b/public/img/avatar_default.png differ diff --git a/public/img/emoji/forgejo.png b/public/img/emoji/forgejo.png new file mode 100644 index 0000000000..f335e51dad Binary files /dev/null and b/public/img/emoji/forgejo.png differ diff --git a/public/img/failed.png b/public/img/failed.png deleted file mode 100644 index b37545f90c..0000000000 Binary files a/public/img/failed.png and /dev/null differ diff --git a/public/img/favicon.png b/public/img/favicon.png index dcd4edb1a3..eda0347eff 100644 Binary files a/public/img/favicon.png and b/public/img/favicon.png differ diff --git a/public/img/favicon.svg b/public/img/favicon.svg index afeeacb77c..804b05e284 100644 --- a/public/img/favicon.svg +++ b/public/img/favicon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/img/forgejo-loading.svg b/public/img/forgejo-loading.svg new file mode 100644 index 0000000000..919552ebb5 --- /dev/null +++ b/public/img/forgejo-loading.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/public/img/forgejo.svg b/public/img/forgejo.svg new file mode 100644 index 0000000000..804b05e284 --- /dev/null +++ b/public/img/forgejo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/gitea-original.svg b/public/img/gitea-original.svg new file mode 100644 index 0000000000..dca9b4f4db --- /dev/null +++ b/public/img/gitea-original.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/gitea.svg b/public/img/gitea.svg index dca9b4f4db..804b05e284 100644 --- a/public/img/gitea.svg +++ b/public/img/gitea.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/img/loading.png b/public/img/loading.png deleted file mode 100644 index c5ba3d9cd7..0000000000 Binary files a/public/img/loading.png and /dev/null differ diff --git a/public/img/logo.png b/public/img/logo.png index c7971f9183..1b2d9b4023 100644 Binary files a/public/img/logo.png and b/public/img/logo.png differ diff --git a/public/img/logo.svg b/public/img/logo.svg index afeeacb77c..804b05e284 100644 --- a/public/img/logo.svg +++ b/public/img/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/releases/Dockerfile b/releases/Dockerfile new file mode 100644 index 0000000000..bef4e4f6de --- /dev/null +++ b/releases/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:3.17 + +RUN echo root > state diff --git a/releases/Dockerfile-rootless b/releases/Dockerfile-rootless new file mode 100644 index 0000000000..561b67e9a8 --- /dev/null +++ b/releases/Dockerfile-rootless @@ -0,0 +1,3 @@ +FROM alpine:3.17 + +RUN echo rootless > state diff --git a/releases/binaries-pull-push-test.sh b/releases/binaries-pull-push-test.sh new file mode 100755 index 0000000000..d7299c8123 --- /dev/null +++ b/releases/binaries-pull-push-test.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +set -ex + +test_teardown() { + setup_api + api DELETE repos/$PUSH_USER/forgejo/releases/tags/$TAG || true + api DELETE repos/$PUSH_USER/forgejo/tags/$TAG || true + rm -fr dist/release + setup_tea + $BIN_DIR/tea login delete $RELEASETEAMUSER || true +} + +test_setup() { + mkdir -p $RELEASE_DIR + touch $RELEASE_DIR/file-one.txt + touch $RELEASE_DIR/file-two.txt +} + +test_ensure_tag() { + api DELETE repos/$PUSH_USER/forgejo/tags/$TAG || true + # + # idempotent + # + ensure_tag + api GET repos/$PUSH_USER/forgejo/tags/$TAG > /tmp/tag1.json + ensure_tag + api GET repos/$PUSH_USER/forgejo/tags/$TAG > /tmp/tag2.json + diff -u /tmp/tag[12].json + # + # sanity check on the SHA of an existing tag + # + ( + CI_COMMIT_SHA=12345 + ! ensure_tag + ) + api DELETE repos/$PUSH_USER/forgejo/tags/$TAG +} + +# +# Running the test locally instead of within Woodpecker +# +# 1. Setup: obtain a token at https://codeberg.org/user/settings/applications +# 2. Run: RELEASETEAMUSER= RELEASETEAMTOKEn= binaries-pull-push-test.sh test_run +# 3. Verify: (optional) manual verification at https://codeberg.org//forgejo/releases +# 4. Cleanup: RELEASETEAMUSER= RELEASETEAMTOKEn= binaries-pull-push-test.sh test_teardown +# +test_run() { + test_teardown + to_push=/tmp/binaries-releases-to-push + pulled=/tmp/binaries-releases-pulled + RELEASE_DIR=$to_push + test_setup + test_ensure_tag + echo "================================ TEST BEGIN" + push + RELEASE_DIR=$pulled + pull + diff -r $to_push $pulled + echo "================================ TEST END" +} + +: ${CI_REPO_OWNER:=dachary} +: ${PULL_USER=$CI_REPO_OWNER} +: ${PUSH_USER=$CI_REPO_OWNER} +: ${CI_COMMIT_TAG:=W17.8.20-1} +: ${CI_COMMIT_SHA:=$(git rev-parse HEAD)} + +. $(dirname $0)/binaries-pull-push.sh diff --git a/releases/binaries-pull-push.sh b/releases/binaries-pull-push.sh new file mode 100755 index 0000000000..1ffb5d8567 --- /dev/null +++ b/releases/binaries-pull-push.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +set -ex + +: ${PULL_USER:=forgejo-integration} +if test "$CI_REPO" = "forgejo/release" ; then + : ${PUSH_USER:=forgejo} +else + : ${PUSH_USER:=forgejo-experimental} +fi +: ${TAG:=${CI_COMMIT_TAG}} +: ${DOMAIN:=codeberg.org} +: ${RELEASE_DIR:=dist/release} +: ${BIN_DIR:=/tmp} +: ${TEA_VERSION:=0.9.0} + + +setup_tea() { + if ! test -f $BIN_DIR/tea ; then + curl -sL https://dl.gitea.io/tea/$TEA_VERSION/tea-$TEA_VERSION-linux-amd64 > $BIN_DIR/tea + chmod +x $BIN_DIR/tea + fi +} + +ensure_tag() { + if api GET repos/$PUSH_USER/forgejo/tags/$TAG > /tmp/tag.json ; then + local sha=$(jq --raw-output .commit.sha < /tmp/tag.json) + if test "$sha" != "$CI_COMMIT_SHA" ; then + cat /tmp/tag.json + echo "the tag SHA in the $PUSH_USER repository does not match the tag SHA that triggered the build: $CI_COMMIT_SHA" + false + fi + else + api POST repos/$PUSH_USER/forgejo/tags --data-raw '{"tag_name": "'$CI_COMMIT_TAG'", "target": "'$CI_COMMIT_SHA'"}' + fi +} + +upload() { + ASSETS=$(ls $RELEASE_DIR/* | sed -e 's/^/-a /') + echo "${CI_COMMIT_TAG}" | grep -qi '\-rc' && export RELEASETYPE="--prerelease" && echo "Uploading as Pre-Release" + echo "${CI_COMMIT_TAG}" | grep -qi '\-test' && export RELEASETYPE="--draft" && echo "Uploading as Draft" + test ${RELEASETYPE+false} || echo "Uploading as Stable" + ensure_tag + anchor=$(echo $CI_COMMIT_TAG | sed -e 's/^v//' -e 's/[^a-zA-Z0-9]/-/g') + $BIN_DIR/tea release create $ASSETS --repo $PUSH_USER/forgejo --note "See https://codeberg.org/forgejo/forgejo/src/branch/forgejo/RELEASE-NOTES.md#${anchor}" --tag $CI_COMMIT_TAG --title $CI_COMMIT_TAG ${RELEASETYPE} +} + +push() { + setup_api + setup_tea + GITEA_SERVER_TOKEN=$RELEASETEAMTOKEN $BIN_DIR/tea login add --name $RELEASETEAMUSER --url $DOMAIN + upload +} + +setup_api() { + if ! which jq || ! which curl ; then + apk --update --no-cache add jq curl + fi +} + +api() { + method=$1 + shift + path=$1 + shift + + curl --fail -X $method -sS -H "Content-Type: application/json" -H "Authorization: token $RELEASETEAMTOKEN" "$@" https://$DOMAIN/api/v1/$path +} + +pull() { + setup_api + ( + mkdir -p $RELEASE_DIR + cd $RELEASE_DIR + api GET repos/$PULL_USER/forgejo/releases/tags/$TAG > /tmp/assets.json + jq --raw-output '.assets[] | "\(.name) \(.browser_download_url)"' < /tmp/assets.json | while read name url ; do + wget --quiet -O $name $url + done + ) +} + + +missing() { + echo need pull or push argument got nothing + exit 1 +} + +${@:-missing} diff --git a/releases/container-images-pull-verify-push-test.sh b/releases/container-images-pull-verify-push-test.sh new file mode 100755 index 0000000000..96e60b33f9 --- /dev/null +++ b/releases/container-images-pull-verify-push-test.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +set -ex + +image_delete() { + curl -sS -H @$TOKEN_HEADER -X DELETE https://$DOMAIN/v2/$1/forgejo/manifests/$2 +} + +# +# Create the same set of images that buildx would +# +test_setup() { + dir=$(dirname $0) + + for suffix in '' '-rootless' ; do + ( + cd $dir + manifests="" + for arch in $ARCHS ; do + image=$(arch_image_name $PULL_USER $arch $suffix) + docker build -f Dockerfile$suffix --platform linux/$arch -t $image . + docker push $image + images="$images $image" + done + manifest=$(image_name $PULL_USER $suffix) + docker manifest rm $manifest || true + docker manifest create $manifest $images + image_put $PULL_USER $(image_tag $suffix) $manifest + ) + done +} + +test_teardown() { + authenticate + for suffix in '' '-rootless' ; do + image_delete $PULL_USER $(image_tag $suffix) + image_delete $PUSH_USER $(image_tag $suffix) + image_delete $PUSH_USER $(short_image_tag $suffix) + for arch in $ARCHS ; do + image_delete $PULL_USER $(arch_image_tag $arch $suffix) + image_delete $PUSH_USER $(arch_image_tag $arch $suffix) + done + done +} + +# +# Running the test locally instead of within Woodpecker +# +# 1. Setup: obtain a token at https://codeberg.org/user/settings/applications +# 2. Run: RELEASETEAMUSER= RELEASETEAMTOKEn= container-images-pull-verify-push-test.sh test_run +# 3. Verify: (optional) manual verification at https://codeberg.org//-/packages/container/forgejo/versions +# 4. Cleanup: RELEASETEAMUSER= RELEASETEAMTOKEn= container-images-pull-verify-push-test.sh test_teardown +# +test_run() { + boot + test_teardown + test_setup + VERIFY_STRING=something + VERIFY_COMMAND="echo $VERIFY_STRING" + echo "================================ TEST BEGIN" + main + echo "================================ TEST END" +} + +: ${CI_REPO_OWNER:=dachary} +: ${PULL_USER:=$CI_REPO_OWNER} +: ${PUSH_USER:=$CI_REPO_OWNER} +: ${CI_COMMIT_TAG:=v17.1.42-2} + +. $(dirname $0)/container-images-pull-verify-push.sh diff --git a/releases/container-images-pull-verify-push.sh b/releases/container-images-pull-verify-push.sh new file mode 100755 index 0000000000..9b2ccc203c --- /dev/null +++ b/releases/container-images-pull-verify-push.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +set -ex + +: ${DOCKER_HOST:=unix:///var/run/docker.sock} +: ${ARCHS:=amd64 arm64} +: ${PULL_USER:=forgejo-integration} +if test "$CI_REPO" = "forgejo/release" ; then + : ${PUSH_USER:=forgejo} +else + : ${PUSH_USER:=forgejo-experimental} +fi +: ${INTEGRATION_IMAGE:=codeberg.org/$PULL_USER/forgejo} +: ${TAG:=${CI_COMMIT_TAG##v}} +: ${SHORT_TAG=${TAG%.*-*}} +: ${DOMAIN:=codeberg.org} +: ${TOKEN_HEADER:=/tmp/token$$} +trap "rm -f ${TOKEN_HEADER}" EXIT + +: ${VERIFY:=true} +VERIFY_COMMAND='gitea --version' +VERIFY_STRING='built with' + +publish() { + for suffix in '' '-rootless' ; do + images="" + for arch in $ARCHS ; do + # + # Get the image from the integration user + # + image=$(image_name $PULL_USER $suffix) + docker pull --platform linux/$arch $image + # + # Verify it is usable + # + if $VERIFY ; then + docker run --platform linux/$arch --rm $image $VERIFY_COMMAND | grep "$VERIFY_STRING" + fi + # + # Push the image with a tag reflecting the architecture to the repo owner + # + arch_image=$(arch_image_name $PUSH_USER $arch $suffix) + docker tag $image $arch_image + docker push $arch_image + images="$images $arch_image" + done + + # + # Push a manifest with all the architectures to the repo owner + # + manifest=$(image_name $PUSH_USER $suffix) + docker manifest rm $manifest || true + docker manifest create $manifest $images + image_put $PUSH_USER $(image_tag $suffix) $manifest + image_put $PUSH_USER $(short_image_tag $suffix) $manifest + # + # Sanity check to ensure the manifest that are published can actualy + # be used. + # + for arch in $ARCHS ; do + docker pull --platform linux/$arch $(image_name $PUSH_USER $suffix) + docker pull --platform linux/$arch $(short_image_name $PUSH_USER $suffix) + done + done +} + +boot() { + if docker version ; then + return + fi + apk --update --no-cache add coredns jq curl + ( echo ".:53 {" ; echo " forward . /etc/resolv.conf"; echo "}" ) > /etc/coredns/Corefile + coredns -conf /etc/coredns/Corefile & + /usr/local/bin/dockerd --data-root /var/lib/docker --host=$DOCKER_HOST --dns 172.17.0.3 & + for i in $(seq 60) ; do + docker version && break + sleep 1 + done + docker version || exit 1 +} + +authenticate() { + echo "$RELEASETEAMTOKEN" | docker login --password-stdin --username "$RELEASETEAMUSER" $DOMAIN + curl -u$RELEASETEAMUSER:$RELEASETEAMTOKEN -sS https://$DOMAIN/v2/token | jq --raw-output '"Authorization: token \(.token)"' > $TOKEN_HEADER +} + +image_put() { + docker manifest inspect $3 > /tmp/manifest.json + curl -sS -H @$TOKEN_HEADER -X PUT --data-binary @/tmp/manifest.json https://$DOMAIN/v2/$1/forgejo/manifests/$2 +} + +main() { + boot + authenticate + publish +} + +image_name() { + echo $DOMAIN/$1/forgejo:$(image_tag $2) +} + +image_tag() { + echo $TAG$1 +} + +short_image_name() { + echo $DOMAIN/$1/forgejo:$(short_image_tag $2) +} + +short_image_tag() { + echo $SHORT_TAG$1 +} + +arch_image_name() { + echo $DOMAIN/$1/forgejo:$(arch_image_tag $2 $3) +} + +arch_image_tag() { + echo $TAG-$1$2 +} + +${@:-main} diff --git a/releases/images/forgejo-v1.18.0-rc1-2-landing.jpg b/releases/images/forgejo-v1.18.0-rc1-2-landing.jpg new file mode 100644 index 0000000000..89f24aa8ec Binary files /dev/null and b/releases/images/forgejo-v1.18.0-rc1-2-landing.jpg differ diff --git a/releases/woodpecker-build/binaries.yml b/releases/woodpecker-build/binaries.yml new file mode 100644 index 0000000000..f140725468 --- /dev/null +++ b/releases/woodpecker-build/binaries.yml @@ -0,0 +1,107 @@ +platform: linux/amd64 + +when: + event: tag + tag: v* + +variables: + - &node_image 'node:18' + - &golang_image 'golang:1.20' + - &alpine_image 'alpine:3.17' + - &gpg_sign_image 'plugins/gpgsign:1' + - &xgo_image 'techknowlogick/xgo:go-1.19.x' + - &gpg_sign_image 'plugins/gpgsign:1' + - &goproxy_override '' + - &goproxy_setup |- + if [ -n "$${GOPROXY_OVERRIDE:-}" ]; then + export GOPROXY="$${GOPROXY_OVERRIDE}"; + echo "Using goproxy from goproxy_override \"$${GOPROXY}\""; + elif [ -n "$${GOPROXY_DEFAULT:-}" ]; then + export GOPROXY="$${GOPROXY_DEFAULT}"; + echo "Using goproxy from goproxy_default (secret) not displaying"; + else + export GOPROXY="https://proxy.golang.org,direct"; + echo "No goproxy overrides or defaults given, using \"$${GOPROXY}\""; + fi + +workspace: + base: /source + path: / + +pipeline: + fetch-tags: + image: *golang_image + pull: true + group: deps + commands: + - git config --add safe.directory '*' + - git fetch --tags --force + + deps-frontend: + image: *node_image + pull: true + group: deps + commands: + - make deps-frontend + + deps-backend: + image: *golang_image + pull: true + group: deps + environment: + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + commands: + - *goproxy_setup + - make deps-backend + + static: + image: *xgo_image + pull: true + commands: + - *goproxy_setup + - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs + - export PATH=$PATH:$GOPATH/bin + - make CI=true LINUX_ARCHS=linux/amd64,linux/arm64,linux/arm-6 release + environment: + TAGS: 'bindata sqlite sqlite_unlock_notify' + DEBIAN_FRONTEND: 'noninteractive' + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + + # + # See https://codeberg.org/forgejo/forgejo/issues/230 for a discussion on this + # compilation stage. The goal is just to verify the build does not break, not that + # the binary produced actually works. + # + freebsd: + image: *xgo_image + group: build + commands: + - *goproxy_setup + - export PATH=$PATH:$GOPATH/bin + - make CI=false release-freebsd + environment: + TAGS: 'bindata sqlite sqlite_unlock_notify' + GOPROXY_OVERRIDE: *goproxy_override + secrets: + - goproxy_default + + verifyruns: + image: *golang_image + commands: + - ./dist/release/forgejo-*-amd64 --version | grep 'built with' + - apt-get update + - apt-get install -y qemu-user-static + - /usr/bin/qemu-aarch64-static ./dist/release/forgejo-*-arm64 --version | grep 'built with' + - /usr/bin/qemu-arm-static ./dist/release/forgejo-*-arm-6 --version | grep 'built with' + + push-integration: + image: *alpine_image + commands: + - PUSH_USER=$CI_REPO_OWNER releases/binaries-pull-push.sh push + secrets: + - releaseteamtoken + - releaseteamuser diff --git a/releases/woodpecker-build/container-images.yml b/releases/woodpecker-build/container-images.yml new file mode 100644 index 0000000000..ebcd7cdc1c --- /dev/null +++ b/releases/woodpecker-build/container-images.yml @@ -0,0 +1,65 @@ +platform: linux/amd64 + +when: + event: tag + tag: v* + +variables: + - &golang_image 'golang:1.20' + - &dind_image 'docker:20.10-dind' + - &buildx_image 'woodpeckerci/plugin-docker-buildx:2.0.0' + - &integration_image 'codeberg.org/forgejo-integration/forgejo' + - &dockerfile_root 'Dockerfile' +# for testing purposes +# - &dockerfile_root 'releases/Dockerfile' + - &dockerfile_rootless 'Dockerfile.rootless' +# for testing purposes +# - &dockerfile_rootless 'releases/Dockerfile-rootless' + - &verify 'true' +# for testing purposes +# - &verify 'false' + - &archs 'amd64 arm64' + +pipeline: + fetch-tags: + image: *golang_image + pull: true + commands: + - git config --add safe.directory '*' + - git fetch --tags --force + + build-root: + image: *buildx_image + group: integration + pull: true + settings: + platforms: linux/amd64,linux/arm64 + dockerfile: *dockerfile_root + registry: + from_secret: domain + tag: ${CI_COMMIT_TAG##v} + repo: *integration_image + build_args: + - GOPROXY=https://proxy.golang.org + password: + from_secret: releaseteamtoken + username: + from_secret: releaseteamuser + + build-rootless: + image: *buildx_image + group: integration + pull: true + settings: + platforms: linux/amd64,linux/arm64 + dockerfile: *dockerfile_rootless + registry: + from_secret: domain + tag: ${CI_COMMIT_TAG##v}-rootless + repo: *integration_image + build_args: + - GOPROXY=https://proxy.golang.org + password: + from_secret: releaseteamtoken + username: + from_secret: releaseteamuser diff --git a/releases/woodpecker-build/releases-helper.yml b/releases/woodpecker-build/releases-helper.yml new file mode 100644 index 0000000000..e700a0f23c --- /dev/null +++ b/releases/woodpecker-build/releases-helper.yml @@ -0,0 +1,34 @@ +platform: linux/amd64 + +when: + event: push + +variables: + - &dind_image 'docker:20.10-dind' + - &alpine_image 'alpine:3.17' + +pipeline: + container-images-pull-verify-push: + image: *dind_image + group: integration + commands: +# arm64 would require qemu-user-static which is not available on alpline +# the test coverage does not change much and running the tests test locally +# is possible if there is a doubt + - ARCHS=amd64 ./releases/container-images-pull-verify-push-test.sh test_run + - ./releases/container-images-pull-verify-push-test.sh test_teardown + secrets: + - releaseteamuser + - releaseteamtoken + - domain + + binaries-pull-push: + image: *alpine_image + group: integration + commands: + - ./releases/binaries-pull-push-test.sh test_run + - ./releases/binaries-pull-push-test.sh test_teardown + secrets: + - releaseteamuser + - releaseteamtoken + - domain diff --git a/releases/woodpecker-publish/binaries.yml b/releases/woodpecker-publish/binaries.yml new file mode 100644 index 0000000000..c269903b85 --- /dev/null +++ b/releases/woodpecker-publish/binaries.yml @@ -0,0 +1,36 @@ +platform: linux/amd64 + +when: + event: tag + +variables: + - &dind_image 'docker:20.10-dind' + - &gpg_sign_image 'plugins/gpgsign:1' + +pipeline: + + pull: + image: *dind_image + commands: + - ./releases/binaries-pull-push.sh pull + + gpg-sign: + image: *gpg_sign_image + pull: true + settings: + detach_sign: true + excludes: + - "dist/release/*.sha256" + files: + - "dist/release/*" + key: + from_secret: releaseteamgpg + + push: + image: *dind_image + commands: + - ./releases/binaries-pull-push.sh push + secrets: + - releaseteamtoken + - releaseteamuser + - domain diff --git a/releases/woodpecker-publish/container-images.yml b/releases/woodpecker-publish/container-images.yml new file mode 100644 index 0000000000..77b1ac2933 --- /dev/null +++ b/releases/woodpecker-publish/container-images.yml @@ -0,0 +1,27 @@ +platform: linux/amd64 + +when: + event: tag + +variables: + - &dind_image 'docker:20.10-dind' + - &integration_image 'codeberg.org/forgejo-integration/forgejo' + - &verify 'true' +# for testing purposes +# - &verify 'false' + - &archs 'amd64 arm64' + +pipeline: + + publish: + image: *dind_image + environment: + INTEGRATION_IMAGE: *integration_image + VERIFY: *verify + ARCHS: *archs + commands: + - ./releases/container-images-pull-verify-push.sh + secrets: + - releaseteamtoken + - releaseteamuser + - domain diff --git a/routers/api/forgejo/v1/api.go b/routers/api/forgejo/v1/api.go new file mode 100644 index 0000000000..2a933450ea --- /dev/null +++ b/routers/api/forgejo/v1/api.go @@ -0,0 +1,18 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1 + +import ( + gocontext "context" + + "code.gitea.io/gitea/modules/web" +) + +func Routes(ctx gocontext.Context) *web.Route { + m := web.NewRoute() + forgejo := NewForgejo() + m.Get("", Root) + m.Get("/version", forgejo.GetVersion) + return m +} diff --git a/routers/api/forgejo/v1/forgejo.go b/routers/api/forgejo/v1/forgejo.go new file mode 100644 index 0000000000..54ab19d7bd --- /dev/null +++ b/routers/api/forgejo/v1/forgejo.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +package v1 + +import ( + "net/http" + + "code.gitea.io/gitea/modules/json" +) + +type Forgejo struct{} + +var _ ServerInterface = &Forgejo{} + +func NewForgejo() *Forgejo { + return &Forgejo{} +} + +var ForgejoVersion = "development" + +func (f *Forgejo) GetVersion(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Version{&ForgejoVersion}) +} diff --git a/routers/api/forgejo/v1/generated.go b/routers/api/forgejo/v1/generated.go new file mode 100644 index 0000000000..afec612e85 --- /dev/null +++ b/routers/api/forgejo/v1/generated.go @@ -0,0 +1,167 @@ +// Package v1 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.12.4 DO NOT EDIT. +package v1 + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" +) + +// Version defines model for Version. +type Version struct { + Number *string `json:"number,omitempty"` +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // API version + // (GET /version) + GetVersion(w http.ResponseWriter, r *http.Request) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// GetVersion operation middleware +func (siw *ServerInterfaceWrapper) GetVersion(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetVersion(w, r) + }) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshallingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshallingParamError) Error() string { + return fmt.Sprintf("Error unmarshalling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshallingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/version", wrapper.GetVersion) + }) + + return r +} diff --git a/routers/api/forgejo/v1/root.go b/routers/api/forgejo/v1/root.go new file mode 100644 index 0000000000..b976c51292 --- /dev/null +++ b/routers/api/forgejo/v1/root.go @@ -0,0 +1,14 @@ +// Copyright The Forgejo Authors. +// SPDX-License-Identifier: MIT + +package v1 + +import ( + "net/http" +) + +func Root(w http.ResponseWriter, r *http.Request) { + // https://www.rfc-editor.org/rfc/rfc8631 + w.Header().Set("Link", "; rel=\"service-desc\"") + w.WriteHeader(http.StatusNoContent) +} diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 6f53bc4ae0..7d1acb5f53 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/packages/composer" @@ -57,7 +58,13 @@ func Routes(ctx gocontext.Context) *web.Route { authGroup := auth.NewGroup(authMethods...) r.Use(func(ctx *context.Context) { - ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + var err error + ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + if err != nil { + log.Error("Verify: %v", err) + ctx.Error(http.StatusUnauthorized, "authGroup.Verify") + return + } ctx.IsSigned = ctx.Doer != nil }) @@ -179,6 +186,7 @@ func Routes(ctx gocontext.Context) *web.Route { r.Group("/maven", func() { r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile) r.Get("/*", maven.DownloadPackageFile) + r.Head("/*", maven.ProvidePackageFileHeader) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/nuget", func() { r.Group("", func() { // Needs to be unauthenticated for the NuGet client. @@ -316,7 +324,13 @@ func ContainerRoutes(ctx gocontext.Context) *web.Route { authGroup := auth.NewGroup(authMethods...) r.Use(func(ctx *context.Context) { - ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + var err error + ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + if err != nil { + log.Error("Failed to verify user: %v", err) + ctx.Error(http.StatusUnauthorized, "Verify") + return + } ctx.IsSigned = ctx.Doer != nil }) diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 86ef7cbd9a..476a2c236a 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -184,7 +184,10 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // UploadPackage creates a new package diff --git a/routers/api/packages/conan/auth.go b/routers/api/packages/conan/auth.go index 00855a97a4..9d3ed98361 100644 --- a/routers/api/packages/conan/auth.go +++ b/routers/api/packages/conan/auth.go @@ -20,22 +20,22 @@ func (a *Auth) Name() string { } // Verify extracts the user from the Bearer token -func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { uid, err := packages.ParseAuthorizationToken(req) if err != nil { log.Trace("ParseAuthorizationToken: %v", err) - return nil + return nil, err } if uid == 0 { - return nil + return nil, nil } u, err := user_model.GetUserByID(uid) if err != nil { log.Error("GetUserByID: %v", err) - return nil + return nil, err } - return u + return u, nil } diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index dd078d6ad3..ac99a48e98 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -473,7 +473,10 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // DeleteRecipeV1 deletes the requested recipe(s) diff --git a/routers/api/packages/container/auth.go b/routers/api/packages/container/auth.go index 770068a3bf..9f99e5b0fb 100644 --- a/routers/api/packages/container/auth.go +++ b/routers/api/packages/container/auth.go @@ -21,25 +21,25 @@ func (a *Auth) Name() string { // Verify extracts the user from the Bearer token // If it's an anonymous session a ghost user is returned -func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { uid, err := packages.ParseAuthorizationToken(req) if err != nil { log.Trace("ParseAuthorizationToken: %v", err) - return nil + return nil, err } if uid == 0 { - return nil + return nil, nil } if uid == -1 { - return user_model.NewGhostUser() + return user_model.NewGhostUser(), nil } u, err := user_model.GetUserByID(uid) if err != nil { log.Error("GetUserByID: %v", err) - return nil + return nil, err } - return u + return u, nil } diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 8a9cbd4a15..72b6857e75 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -7,8 +7,11 @@ package container import ( "context" "encoding/hex" + "errors" "fmt" + "os" "strings" + "sync" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -16,9 +19,12 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" + "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" ) +var uploadVersionMutex sync.Mutex + // saveAsPackageBlob creates a package blob from an upload // The uploaded blob gets stored in a special upload version to link them to the package/image func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_service.PackageInfo) (*packages_model.PackageBlob, error) { @@ -28,6 +34,65 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic contentStore := packages_module.NewContentStore() + uploadVersion, err := getOrCreateUploadVersion(pi) + if err != nil { + return nil, err + } + + err = db.WithTx(func(ctx context.Context) error { + pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb) + if err != nil { + log.Error("Error inserting package blob: %v", err) + return err + } + // FIXME: Workaround to be removed in v1.20 + // https://github.com/go-gitea/gitea/issues/19586 + if exists { + err = contentStore.Has(packages_module.BlobHash256Key(pb.HashSHA256)) + if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) { + log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256) + exists = false + } + } + if !exists { + if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), hsr, hsr.Size()); err != nil { + log.Error("Error saving package blob in content store: %v", err) + return err + } + } + + return createFileForBlob(ctx, uploadVersion, pb) + }) + if err != nil { + if !exists { + if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { + log.Error("Error deleting package blob from content store: %v", err) + } + } + return nil, err + } + + return pb, nil +} + +// mountBlob mounts the specific blob to a different package +func mountBlob(pi *packages_service.PackageInfo, pb *packages_model.PackageBlob) error { + uploadVersion, err := getOrCreateUploadVersion(pi) + if err != nil { + return err + } + + return db.WithTx(func(ctx context.Context) error { + return createFileForBlob(ctx, uploadVersion, pb) + }) +} + +func getOrCreateUploadVersion(pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) { + var uploadVersion *packages_model.PackageVersion + + // FIXME: Replace usage of mutex with database transaction + // https://github.com/go-gitea/gitea/pull/21862 + uploadVersionMutex.Lock() err := db.WithTx(func(ctx context.Context) error { created := true p := &packages_model.Package{ @@ -68,52 +133,40 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic } } - pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb) - if err != nil { - log.Error("Error inserting package blob: %v", err) - return err - } - if !exists { - if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), hsr, hsr.Size()); err != nil { - log.Error("Error saving package blob in content store: %v", err) - return err - } - } - - filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256)) - - pf := &packages_model.PackageFile{ - VersionID: pv.ID, - BlobID: pb.ID, - Name: filename, - LowerName: filename, - CompositeKey: packages_model.EmptyFileKey, - } - if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { - return nil - } - log.Error("Error inserting package file: %v", err) - return err - } - - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil { - log.Error("Error setting package file property: %v", err) - return err - } + uploadVersion = pv return nil }) - if err != nil { - if !exists { - if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { - log.Error("Error deleting package blob from content store: %v", err) - } + uploadVersionMutex.Unlock() + + return uploadVersion, err +} + +func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, pb *packages_model.PackageBlob) error { + filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256)) + + pf := &packages_model.PackageFile{ + VersionID: pv.ID, + BlobID: pb.ID, + Name: filename, + LowerName: filename, + CompositeKey: packages_model.EmptyFileKey, + } + var err error + if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { + if err == packages_model.ErrDuplicatePackageFile { + return nil } - return nil, err + log.Error("Error inserting package file: %v", err) + return err } - return pb, nil + if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil { + log.Error("Error setting package file property: %v", err) + return err + } + + return nil } func deleteBlob(ownerID int64, image, digest string) error { diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 5bc64e1b29..31236ff0d3 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "net/url" + "os" "regexp" "strconv" "strings" @@ -22,18 +23,23 @@ import ( "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" - "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" + + digest "github.com/opencontainers/go-digest" ) // maximum size of a container manifest // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests const maxManifestSize = 10 * 1024 * 1024 -var imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) +var ( + imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) + referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) +) type containerHeaders struct { Status int @@ -193,11 +199,16 @@ func InitiateUploadBlob(ctx *context.Context) { mount := ctx.FormTrim("mount") from := ctx.FormTrim("from") if mount != "" { - blob, _ := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ - Image: from, - Digest: mount, + blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ + Repository: from, + Digest: mount, }) if blob != nil { + if err := mountBlob(&packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + setResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount), ContentDigest: mount, @@ -400,16 +411,16 @@ func CancelUploadBlob(ctx *context.Context) { } func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { - digest := ctx.Params("digest") + d := ctx.Params("digest") - if !oci.Digest(digest).Validate() { + if digest.Digest(d).Validate() != nil { return nil, container_model.ErrContainerBlobNotExist } - return container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ + return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.Params("image"), - Digest: digest, + Digest: d, }) } @@ -464,14 +475,14 @@ func GetBlob(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs func DeleteBlob(ctx *context.Context) { - digest := ctx.Params("digest") + d := ctx.Params("digest") - if !oci.Digest(digest).Validate() { + if digest.Digest(d).Validate() != nil { apiErrorDefined(ctx, errBlobUnknown) return } - if err := deleteBlob(ctx.Package.Owner.ID, ctx.Params("image"), digest); err != nil { + if err := deleteBlob(ctx.Package.Owner.ID, ctx.Params("image"), d); err != nil { apiError(ctx, http.StatusInternalServerError, err) return } @@ -486,15 +497,15 @@ func UploadManifest(ctx *context.Context) { reference := ctx.Params("reference") mci := &manifestCreationInfo{ - MediaType: oci.MediaType(ctx.Req.Header.Get("Content-Type")), + MediaType: ctx.Req.Header.Get("Content-Type"), Owner: ctx.Package.Owner, Creator: ctx.Doer, Image: ctx.Params("image"), Reference: reference, - IsTagged: !oci.Digest(reference).Validate(), + IsTagged: digest.Digest(reference).Validate() != nil, } - if mci.IsTagged && !oci.Reference(reference).Validate() { + if mci.IsTagged && !referencePattern.MatchString(reference) { apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid")) return } @@ -532,7 +543,7 @@ func UploadManifest(ctx *context.Context) { }) } -func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { +func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) { reference := ctx.Params("reference") opts := &container_model.BlobSearchOptions{ @@ -540,15 +551,25 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe Image: ctx.Params("image"), IsManifest: true, } - if oci.Digest(reference).Validate() { + + if digest.Digest(reference).Validate() == nil { opts.Digest = reference - } else if oci.Reference(reference).Validate() { + } else if referencePattern.MatchString(reference) { opts.Tag = reference } else { return nil, container_model.ErrContainerBlobNotExist } - return container_model.GetContainerBlob(ctx, opts) + return opts, nil +} + +func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { + opts, err := getBlobSearchOptionsFromContext(ctx) + if err != nil { + return nil, err + } + + return workaroundGetContainerBlob(ctx, opts) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry @@ -604,18 +625,8 @@ func GetManifest(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests func DeleteManifest(ctx *context.Context) { - reference := ctx.Params("reference") - - opts := &container_model.BlobSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Image: ctx.Params("image"), - IsManifest: true, - } - if oci.Digest(reference).Validate() { - opts.Digest = reference - } else if oci.Reference(reference).Validate() { - opts.Tag = reference - } else { + opts, err := getBlobSearchOptionsFromContext(ctx) + if err != nil { apiErrorDefined(ctx, errManifestUnknown) return } @@ -688,3 +699,23 @@ func GetTagList(ctx *context.Context) { Tags: tags, }) } + +// FIXME: Workaround to be removed in v1.20 +// https://github.com/go-gitea/gitea/issues/19586 +func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) { + blob, err := container_model.GetContainerBlob(ctx, opts) + if err != nil { + return nil, err + } + + err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256)) + if err != nil { + if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) { + log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256) + return nil, container_model.ErrContainerBlobNotExist + } + return nil, err + } + + return blob, nil +} diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 8beed3dbb7..468cfd40a1 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -6,8 +6,10 @@ package container import ( "context" + "errors" "fmt" "io" + "os" "strings" "code.gitea.io/gitea/models/db" @@ -16,15 +18,31 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/notification" packages_module "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" - "code.gitea.io/gitea/modules/packages/container/oci" + "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" + + digest "github.com/opencontainers/go-digest" + oci "github.com/opencontainers/image-spec/specs-go/v1" ) +func isValidMediaType(mt string) bool { + return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.") +} + +func isImageManifestMediaType(mt string) bool { + return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json") +} + +func isImageIndexMediaType(mt string) bool { + return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json") +} + // manifestCreationInfo describes a manifest to create type manifestCreationInfo struct { - MediaType oci.MediaType + MediaType string Owner *user_model.User Creator *user_model.User Image string @@ -34,12 +52,12 @@ type manifestCreationInfo struct { } func processManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { - var schema oci.SchemaMediaBase - if err := json.NewDecoder(buf).Decode(&schema); err != nil { + var index oci.Index + if err := json.NewDecoder(buf).Decode(&index); err != nil { return "", err } - if schema.SchemaVersion != 2 { + if index.SchemaVersion != 2 { return "", errUnsupported.WithMessage("Schema version is not supported") } @@ -47,19 +65,17 @@ func processManifest(mci *manifestCreationInfo, buf *packages_module.HashedBuffe return "", err } - if !mci.MediaType.IsValid() { - mci.MediaType = schema.MediaType - if !mci.MediaType.IsValid() { + if !isValidMediaType(mci.MediaType) { + mci.MediaType = index.MediaType + if !isValidMediaType(mci.MediaType) { return "", errManifestInvalid.WithMessage("MediaType not recognized") } } - if mci.MediaType.IsImageManifest() { - d, err := processImageManifest(mci, buf) - return d, err - } else if mci.MediaType.IsImageIndex() { - d, err := processImageManifestIndex(mci, buf) - return d, err + if isImageManifestMediaType(mci.MediaType) { + return processImageManifest(mci, buf) + } else if isImageIndexMediaType(mci.MediaType) { + return processImageManifestIndex(mci, buf) } return "", errManifestInvalid } @@ -166,6 +182,10 @@ func processImageManifest(mci *manifestCreationInfo, buf *packages_module.Hashed return err } + if err := notifyPackageCreate(mci.Creator, pv); err != nil { + return err + } + manifestDigest = digest return nil @@ -202,7 +222,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H } for _, manifest := range index.Manifests { - if !manifest.MediaType.IsImageManifest() { + if !isImageManifestMediaType(manifest.MediaType) { return errManifestInvalid } @@ -255,6 +275,10 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H return err } + if err := notifyPackageCreate(mci.Creator, pv); err != nil { + return err + } + manifestDigest = digest return nil @@ -266,6 +290,17 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H return manifestDigest, nil } +func notifyPackageCreate(doer *user_model.User, pv *packages_model.PackageVersion) error { + pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv) + if err != nil { + return err + } + + notification.NotifyPackageCreate(doer, pd) + + return nil +} + func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) { created := true p := &packages_model.Package{ @@ -342,8 +377,8 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } type blobReference struct { - Digest oci.Digest - MediaType oci.MediaType + Digest digest.Digest + MediaType string Name string File *packages_model.PackageFileDescriptor ExpectedSize int64 @@ -377,7 +412,7 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } props := map[string]string{ - container_module.PropertyMediaType: string(ref.MediaType), + container_module.PropertyMediaType: ref.MediaType, container_module.PropertyDigest: string(ref.Digest), } for name, value := range props { @@ -403,6 +438,15 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack log.Error("Error inserting package blob: %v", err) return nil, false, "", err } + // FIXME: Workaround to be removed in v1.20 + // https://github.com/go-gitea/gitea/issues/19586 + if exists { + err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256)) + if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) { + log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256) + exists = false + } + } if !exists { contentStore := packages_module.NewContentStore() if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil { @@ -413,7 +457,7 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack manifestDigest := digestFromHashSummer(buf) err = createFileFromBlobReference(ctx, pv, nil, &blobReference{ - Digest: oci.Digest(manifestDigest), + Digest: digest.Digest(manifestDigest), MediaType: mci.MediaType, Name: container_model.ManifestFilename, File: &packages_model.PackageFileDescriptor{Blob: pb}, diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 81891bec26..f2bc1dc597 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -53,7 +53,10 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // UploadPackage uploads the specific generic package. diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go index 9c85e0874f..43dafe6296 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -138,7 +138,10 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // UploadPackage creates a new package diff --git a/routers/api/packages/maven/api.go b/routers/api/packages/maven/api.go index b60a317814..4ca541dd6f 100644 --- a/routers/api/packages/maven/api.go +++ b/routers/api/packages/maven/api.go @@ -6,7 +6,6 @@ package maven import ( "encoding/xml" - "sort" "strings" packages_model "code.gitea.io/gitea/models/packages" @@ -23,12 +22,8 @@ type MetadataResponse struct { Version []string `xml:"versioning>versions>version"` } +// pds is expected to be sorted ascending by CreatedUnix func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse { - sort.Slice(pds, func(i, j int) bool { - // Maven and Gradle order packages by their creation timestamp and not by their version string - return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix - }) - var release *packages_model.PackageDescriptor versions := make([]string, 0, len(pds)) diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index bf00c199f5..de88328806 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -16,6 +16,8 @@ import ( "net/http" "path/filepath" "regexp" + "sort" + "strconv" "strings" packages_model "code.gitea.io/gitea/models/packages" @@ -34,6 +36,10 @@ const ( extensionSHA1 = ".sha1" extensionSHA256 = ".sha256" extensionSHA512 = ".sha512" + extensionPom = ".pom" + extensionJar = ".jar" + contentTypeJar = "application/java-archive" + contentTypeXML = "text/xml" ) var ( @@ -49,6 +55,15 @@ func apiError(ctx *context.Context, status int, obj interface{}) { // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { + handlePackageFile(ctx, true) +} + +// ProvidePackageFileHeader provides only the headers describing a package +func ProvidePackageFileHeader(ctx *context.Context) { + handlePackageFile(ctx, false) +} + +func handlePackageFile(ctx *context.Context, serveContent bool) { params, err := extractPathParameters(ctx) if err != nil { apiError(ctx, http.StatusBadRequest, err) @@ -58,7 +73,7 @@ func DownloadPackageFile(ctx *context.Context) { if params.IsMeta && params.Version == "" { serveMavenMetadata(ctx, params) } else { - servePackageFile(ctx, params) + servePackageFile(ctx, params, serveContent) } } @@ -82,6 +97,11 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { return } + sort.Slice(pds, func(i, j int) bool { + // Maven and Gradle order packages by their creation timestamp and not by their version string + return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix + }) + xmlMetadata, err := xml.Marshal(createMetadataResponse(pds)) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -89,6 +109,9 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...) + latest := pds[len(pds)-1] + ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat)) + ext := strings.ToLower(filepath.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte @@ -110,10 +133,15 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { return } - ctx.PlainTextBytes(http.StatusOK, xmlMetadataWithHeader) + ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader))) + ctx.Resp.Header().Set("Content-Type", contentTypeXML) + + if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil { + log.Error("write bytes failed: %v", err) + } } -func servePackageFile(ctx *context.Context, params parameters) { +func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { packageName := params.GroupID + "-" + params.ArtifactID pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) @@ -165,6 +193,23 @@ func servePackageFile(ctx *context.Context, params parameters) { return } + opts := &context.ServeHeaderOptions{ + ContentLength: &pb.Size, + LastModified: pf.CreatedUnix.AsLocalTime(), + } + switch ext { + case extensionJar: + opts.ContentType = contentTypeJar + case extensionPom: + opts.ContentType = contentTypeXML + } + + if !serveContent { + ctx.SetServeHeaders(opts) + ctx.Status(http.StatusOK) + return + } + s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(pb.HashSHA256)) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -177,7 +222,9 @@ func servePackageFile(ctx *context.Context, params parameters) { } } - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + opts.Filename = pf.Name + + ctx.ServeContent(s, opts) } // UploadPackageFile adds a file to the package. If the package does not exist, it gets created. @@ -272,7 +319,7 @@ func UploadPackageFile(ctx *context.Context) { } // If it's the package pom file extract the metadata - if ext == ".pom" { + if ext == extensionPom { pfci.IsLead = true var err error diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 82dae0cf43..502d353ffc 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -103,7 +103,10 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // DownloadPackageFileByName finds the version and serves the contents of a package @@ -146,7 +149,10 @@ func DownloadPackageFileByName(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // UploadPackage creates a new package @@ -396,8 +402,9 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo func PackageSearch(ctx *context.Context) { pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Type: packages_model.TypeNpm, + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeNpm, + IsInternal: util.OptionalBoolFalse, Name: packages_model.SearchValue{ ExactMatch: false, Value: ctx.FormTrim("text"), diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go index 60a5d9c0e4..31ed7afb52 100644 --- a/routers/api/packages/nuget/api_v2.go +++ b/routers/api/packages/nuget/api_v2.go @@ -345,7 +345,7 @@ func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNames Content: content, Properties: &FeedEntryProperties{ Version: pd.Version.Version, - NormalizedVersion: normalizeVersion(pd.SemVer), + NormalizedVersion: pd.Version.Version, Authors: metadata.Authors, Dependencies: buildDependencyString(metadata), Description: metadata.Description, diff --git a/routers/api/packages/nuget/api_v3.go b/routers/api/packages/nuget/api_v3.go index bb3e447bd6..62b4c75131 100644 --- a/routers/api/packages/nuget/api_v3.go +++ b/routers/api/packages/nuget/api_v3.go @@ -5,15 +5,11 @@ package nuget import ( - "bytes" - "fmt" "sort" "time" packages_model "code.gitea.io/gitea/models/packages" nuget_module "code.gitea.io/gitea/modules/packages/nuget" - - "github.com/hashicorp/go-version" ) // https://docs.microsoft.com/en-us/nuget/api/service-index#resources @@ -96,8 +92,8 @@ func createRegistrationIndexResponse(l *linkBuilder, pds []*packages_model.Packa { RegistrationPageURL: l.GetRegistrationIndexURL(pds[0].Package.Name), Count: len(pds), - Lower: normalizeVersion(pds[0].SemVer), - Upper: normalizeVersion(pds[len(pds)-1].SemVer), + Lower: pds[0].Version.Version, + Upper: pds[len(pds)-1].Version.Version, Items: items, }, }, @@ -174,7 +170,7 @@ type PackageVersionsResponse struct { func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *PackageVersionsResponse { versions := make([]string, 0, len(pds)) for _, pd := range pds { - versions = append(versions, normalizeVersion(pd.SemVer)) + versions = append(versions, pd.Version.Version) } return &PackageVersionsResponse{ @@ -249,15 +245,3 @@ func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name), } } - -// normalizeVersion removes the metadata -func normalizeVersion(v *version.Version) string { - var buf bytes.Buffer - segments := v.Segments64() - fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) - pre := v.Prerelease() - if pre != "" { - fmt.Fprintf(&buf, "-%s", pre) - } - return buf.String() -} diff --git a/routers/api/packages/nuget/auth.go b/routers/api/packages/nuget/auth.go index 1dad452648..859ac16fe6 100644 --- a/routers/api/packages/nuget/auth.go +++ b/routers/api/packages/nuget/auth.go @@ -21,19 +21,20 @@ func (a *Auth) Name() string { } // https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters -func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { token, err := auth_model.GetAccessTokenBySHA(req.Header.Get("X-NuGet-ApiKey")) if err != nil { if !(auth_model.IsErrAccessTokenNotExist(err) || auth_model.IsErrAccessTokenEmpty(err)) { log.Error("GetAccessTokenBySHA: %v", err) + return nil, err } - return nil + return nil, nil } u, err := user_model.GetUserByID(token.UID) if err != nil { log.Error("GetUserByID: %v", err) - return nil + return nil, err } token.UpdatedUnix = timeutil.TimeStampNow() @@ -41,5 +42,5 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS log.Error("UpdateAccessToken: %v", err) } - return u + return u, nil } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index e84aef3160..5267bca8f5 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -342,7 +342,10 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // UploadPackage creates a new package with the metadata contained in the uploaded nupgk file @@ -552,7 +555,10 @@ func DownloadSymbolFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // DeletePackage hard deletes the package diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go index 9af0ceeb0e..672669c985 100644 --- a/routers/api/packages/pub/pub.go +++ b/routers/api/packages/pub/pub.go @@ -271,5 +271,8 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 3a046abe18..e6cc21799a 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -21,12 +21,21 @@ import ( packages_service "code.gitea.io/gitea/services/packages" ) -// https://www.python.org/dev/peps/pep-0503/#normalized-names -var normalizer = strings.NewReplacer(".", "-", "_", "-") -var nameMatcher = regexp.MustCompile(`\A[a-zA-Z0-9\.\-_]+\z`) +// https://peps.python.org/pep-0426/#name +var ( + normalizer = strings.NewReplacer(".", "-", "_", "-") + nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`) +) -// https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions -var versionMatcher = regexp.MustCompile(`^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$`) +// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions +var versionMatcher = regexp.MustCompile(`\Av?` + + `(?:[0-9]+!)?` + // epoch + `[0-9]+(?:\.[0-9]+)*` + // release segment + `(?:[-_\.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_\.]?[0-9]*)?` + // pre-release + `(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release + `(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release + `(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version + `\z`) func apiError(ctx *context.Context, status int, obj interface{}) { helper.LogAndProcessError(ctx, status, obj, func(message string) { @@ -88,7 +97,10 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // UploadPackageFile adds a file to the package. If the package does not exist, it gets created. @@ -121,7 +133,7 @@ func UploadPackageFile(ctx *context.Context) { packageName := normalizer.Replace(ctx.Req.FormValue("name")) packageVersion := ctx.Req.FormValue("version") - if !nameMatcher.MatchString(packageName) || !versionMatcher.MatchString(packageVersion) { + if !isValidNameAndVersion(packageName, packageVersion) { apiError(ctx, http.StatusBadRequest, "invalid name or version") return } @@ -139,7 +151,7 @@ func UploadPackageFile(ctx *context.Context) { Name: packageName, Version: packageVersion, }, - SemverCompatible: true, + SemverCompatible: false, Creator: ctx.Doer, Metadata: &pypi_module.Metadata{ Author: ctx.Req.FormValue("author"), @@ -170,3 +182,7 @@ func UploadPackageFile(ctx *context.Context) { ctx.Status(http.StatusCreated) } + +func isValidNameAndVersion(packageName, packageVersion string) bool { + return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion) +} diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go new file mode 100644 index 0000000000..56e327a347 --- /dev/null +++ b/routers/api/packages/pypi/pypi_test.go @@ -0,0 +1,39 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package pypi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidNameAndVersion(t *testing.T) { + // The test cases below were created from the following Python PEPs: + // https://peps.python.org/pep-0426/#name + // https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions + + // Valid Cases + assert.True(t, isValidNameAndVersion("A", "1.0.1")) + assert.True(t, isValidNameAndVersion("Test.Name.1234", "1.0.1")) + assert.True(t, isValidNameAndVersion("test_name", "1.0.1")) + assert.True(t, isValidNameAndVersion("test-name", "1.0.1")) + assert.True(t, isValidNameAndVersion("test-name", "v1.0.1")) + assert.True(t, isValidNameAndVersion("test-name", "2012.4")) + assert.True(t, isValidNameAndVersion("test-name", "1.0.1-alpha")) + assert.True(t, isValidNameAndVersion("test-name", "1.0.1a1")) + assert.True(t, isValidNameAndVersion("test-name", "1.0b2.r345.dev456")) + assert.True(t, isValidNameAndVersion("test-name", "1!1.0.1")) + assert.True(t, isValidNameAndVersion("test-name", "1.0.1+local.1")) + + // Invalid Cases + assert.False(t, isValidNameAndVersion(".test-name", "1.0.1")) + assert.False(t, isValidNameAndVersion("test!name", "1.0.1")) + assert.False(t, isValidNameAndVersion("-test-name", "1.0.1")) + assert.False(t, isValidNameAndVersion("test-name-", "1.0.1")) + assert.False(t, isValidNameAndVersion("test-name", "a1.0.1")) + assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa")) + assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta")) +} diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 319c94b91f..d83120ca2f 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -77,7 +77,9 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo }) } - ctx.SetServeHeaders(filename + ".gz") + ctx.SetServeHeaders(&context.ServeHeaderOptions{ + Filename: filename + ".gz", + }) zw := gzip.NewWriter(ctx.Resp) defer zw.Close() @@ -115,7 +117,9 @@ func ServePackageSpecification(ctx *context.Context) { return } - ctx.SetServeHeaders(filename) + ctx.SetServeHeaders(&context.ServeHeaderOptions{ + Filename: filename, + }) zw := zlib.NewWriter(ctx.Resp) defer zw.Close() @@ -188,7 +192,10 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } // UploadPackageFile adds a file to the package. If the package does not exist, it gets created. diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go index 7750e5dc4b..35643e3150 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -235,5 +235,8 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 1a4b020011..3faca09404 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -16,10 +16,10 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0d11674aa9..38cd8e87bd 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -232,13 +232,10 @@ func reqExploreSignIn() func(ctx *context.APIContext) { } } -func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { +func reqBasicAuth() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if ctx.IsSigned && setting.Service.EnableReverseProxyAuth && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { - return - } if !ctx.Context.IsBasicAuth { - ctx.Error(http.StatusUnauthorized, "reqBasicOrRevProxyAuth", "auth required") + ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required") return } ctx.CheckForOTP() @@ -597,9 +594,6 @@ func buildAuthGroup() *auth.Group { &auth.HTTPSign{}, &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API ) - if setting.Service.EnableReverseProxyAuth { - group.Add(&auth.ReverseProxy{}) - } specialAdd(group) return group @@ -617,7 +611,7 @@ func Routes(ctx gocontext.Context) *web.Route { // setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option AllowedMethods: setting.CORSConfig.Methods, AllowCredentials: setting.CORSConfig.AllowCredentials, - AllowedHeaders: []string{"Authorization", "X-Gitea-OTP"}, + AllowedHeaders: []string{"Authorization", "X-Gitea-OTP", "X-Forgejo-OTP"}, MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), })) } @@ -689,7 +683,7 @@ func Routes(ctx gocontext.Context) *web.Route { m.Combo("").Get(user.ListAccessTokens). Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken) m.Combo("/{id}").Delete(user.DeleteAccessToken) - }, reqBasicOrRevProxyAuth()) + }, reqBasicAuth()) }, context_service.UserAssignmentAPI()) }) @@ -898,7 +892,7 @@ func Routes(ctx gocontext.Context) *web.Route { m.Group("/{index}", func() { m.Combo("").Get(repo.GetIssue). Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue). - Delete(reqToken(), reqAdmin(), repo.DeleteIssue) + Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue) m.Group("/comments", func() { m.Combo("").Get(repo.ListIssueComments). Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) @@ -1048,7 +1042,7 @@ func Routes(ctx gocontext.Context) *web.Route { m.Get("/blobs/{sha}", repo.GetBlob) m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) - }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) + }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch) m.Group("/contents", func() { m.Get("", repo.GetContentsList) diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go index bd629b87ca..9a4bd36203 100644 --- a/routers/api/v1/misc/nodeinfo.go +++ b/routers/api/v1/misc/nodeinfo.go @@ -66,10 +66,10 @@ func NodeInfo(ctx *context.APIContext) { nodeInfo := &structs.NodeInfo{ Version: "2.1", Software: structs.NodeInfoSoftware{ - Name: "gitea", + Name: "forgejo", Version: setting.AppVer, - Repository: "https://github.com/go-gitea/gitea.git", - Homepage: "https://gitea.io/", + Repository: "https://codeberg.org/forgejo/forgejo.git", + Homepage: "https://forgejo.org/", }, Protocols: []string{"activitypub"}, Services: structs.NodeInfoServices{ diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 84a172e92b..b0ef5bc96e 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -252,42 +252,50 @@ func ListBranches(ctx *context.APIContext) { // "200": // "$ref": "#/responses/BranchList" - listOptions := utils.GetListOptions(ctx) - skip, _ := listOptions.GetStartEnd() - branches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize) - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranches", err) - return - } + var totalNumOfBranches int + var apiBranches []*api.Branch - apiBranches := make([]*api.Branch, 0, len(branches)) - for i := range branches { - c, err := branches[i].GetCommit() + listOptions := utils.GetListOptions(ctx) + + if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil { + skip, _ := listOptions.GetStartEnd() + branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize) if err != nil { - // Skip if this branch doesn't exist anymore. - if git.IsErrNotExist(err) { - totalNumOfBranches-- - continue + ctx.Error(http.StatusInternalServerError, "GetBranches", err) + return + } + + apiBranches = make([]*api.Branch, 0, len(branches)) + for i := range branches { + c, err := branches[i].GetCommit() + if err != nil { + // Skip if this branch doesn't exist anymore. + if git.IsErrNotExist(err) { + total-- + continue + } + ctx.Error(http.StatusInternalServerError, "GetCommit", err) + return } - ctx.Error(http.StatusInternalServerError, "GetCommit", err) - return + branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branches[i].Name) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err) + return + } + apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) + return + } + apiBranches = append(apiBranches, apiBranch) } - branchProtection, err := git_model.GetProtectedBranchBy(ctx, ctx.Repo.Repository.ID, branches[i].Name) - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) - return - } - apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) - if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) - return - } - apiBranches = append(apiBranches, apiBranch) + + totalNumOfBranches = total } ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize) ctx.SetTotalCountHeader(int64(totalNumOfBranches)) - ctx.JSON(http.StatusOK, &apiBranches) + ctx.JSON(http.StatusOK, apiBranches) } // GetBranchProtection gets a branch protection diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 6dead81e6d..3dcd28ccd0 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -33,7 +33,10 @@ import ( files_service "code.gitea.io/gitea/services/repository/files" ) -const giteaObjectTypeHeader = "X-Gitea-Object-Type" +const ( + giteaObjectTypeHeader = "X-Gitea-Object-Type" + forgejoObjectTypeHeader = "X-Forgejo-Object-Type" +) // GetRawFile get a file by path on a repository func GetRawFile(ctx *context.APIContext) { @@ -80,6 +83,7 @@ func GetRawFile(ctx *context.APIContext) { } ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) + ctx.RespHeader().Set(forgejoObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil { ctx.Error(http.StatusInternalServerError, "ServeBlob", err) @@ -129,6 +133,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { } ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) + ctx.RespHeader().Set(forgejoObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file if blob.Size() > 1024 { @@ -341,7 +346,11 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. return } defer fr.Close() - ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime()) + + ctx.ServeContent(fr, &context.ServeHeaderOptions{ + Filename: downloadName, + LastModified: archiver.CreatedUnix.AsLocalTime(), + }) } // GetEditorconfig get editor config of a repository diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 91e5e0c031..56bcd07cb5 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -346,10 +346,11 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro } pushMirror := &repo_model.PushMirror{ - RepoID: repo.ID, - Repo: repo, - RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix), - Interval: interval, + RepoID: repo.ID, + Repo: repo, + RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix), + Interval: interval, + SyncOnCommit: mirrorOption.SyncOnCommit, } if err = repo_model.InsertPushMirror(ctx, pushMirror); err != nil { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index f6507dceba..a687eabfba 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -491,6 +491,11 @@ func EditPullRequest(ctx *context.APIContext) { issue := pr.Issue issue.Repo = ctx.Repo.Repository + if err := issue.LoadAttributes(ctx); err != nil { + ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + return + } + if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) { ctx.Status(http.StatusForbidden) return @@ -1443,7 +1448,11 @@ func GetPullRequestFiles(ctx *context.APIContext) { end = totalNumberOfFiles } - apiFiles := make([]*api.ChangedFile, 0, end-start) + lenFiles := end - start + if lenFiles < 0 { + lenFiles = 0 + } + apiFiles := make([]*api.ChangedFile, 0, lenFiles) for i := start; i < end; i++ { apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID)) } diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 97ef69a6ea..7bc0edeadc 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -184,6 +184,7 @@ func getCommitStatuses(ctx *context.APIContext, sha string) { ctx.Error(http.StatusBadRequest, "ref/sha not given", nil) return } + sha = utils.MustConvertToSHA1(ctx.Context, sha) repo := ctx.Repo.Repository listOptions := utils.GetListOptions(ctx) diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go index 816f8b3595..d05a86c0cd 100644 --- a/routers/api/v1/utils/git.go +++ b/routers/api/v1/utils/git.go @@ -34,6 +34,8 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string { } } + sha = MustConvertToSHA1(ctx.Context, sha) + if ctx.Repo.GitRepo != nil { err := ctx.Repo.GitRepo.AddLastCommitCache(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, ref != sha), ctx.Repo.Repository.FullName(), sha) if err != nil { @@ -66,3 +68,30 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str } return "", "", nil } + +// ConvertToSHA1 returns a full-length SHA1 from a potential ID string +func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) { + if len(commitID) == git.SHAFullLength && git.IsValidSHAPattern(commitID) { + sha1, err := git.NewIDFromString(commitID) + if err == nil { + return sha1, nil + } + } + + gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, ctx.Repo.Repository.RepoPath()) + if err != nil { + return git.SHA1{}, fmt.Errorf("RepositoryFromContextOrOpen: %w", err) + } + defer closer.Close() + + return gitRepo.ConvertToSHA1(commitID) +} + +// MustConvertToSHA1 returns a full-length SHA1 string from a potential ID string, or returns origin input if it can't convert to SHA1 +func MustConvertToSHA1(ctx *context.Context, commitID string) string { + sha, err := ConvertToSHA1(ctx, commitID) + if err != nil { + return commitID + } + return sha.String() +} diff --git a/routers/common/repo.go b/routers/common/repo.go index a9e80fad48..340eb1809f 100644 --- a/routers/common/repo.go +++ b/routers/common/repo.go @@ -5,9 +5,7 @@ package common import ( - "fmt" "io" - "net/url" "path" "path/filepath" "strings" @@ -53,50 +51,44 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read buf = buf[:n] } - httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute) + opts := &context.ServeHeaderOptions{ + Filename: path.Base(filePath), + } if size >= 0 { - ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size)) + opts.ContentLength = &size } else { log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size) } - fileName := path.Base(filePath) sniffedType := typesniffer.DetectContentType(buf) isPlain := sniffedType.IsText() || ctx.FormBool("render") - mimeType := "" - charset := "" if setting.MimeTypeMap.Enabled { - fileExtension := strings.ToLower(filepath.Ext(fileName)) - mimeType = setting.MimeTypeMap.Map[fileExtension] + fileExtension := strings.ToLower(filepath.Ext(filePath)) + opts.ContentType = setting.MimeTypeMap.Map[fileExtension] } - if mimeType == "" { + if opts.ContentType == "" { if sniffedType.IsBrowsableBinaryType() { - mimeType = sniffedType.GetMimeType() + opts.ContentType = sniffedType.GetMimeType() } else if isPlain { - mimeType = "text/plain" + opts.ContentType = "text/plain" } else { - mimeType = typesniffer.ApplicationOctetStream + opts.ContentType = typesniffer.ApplicationOctetStream } } if isPlain { + var charset string charset, err = charsetModule.DetectEncoding(buf) if err != nil { log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err) charset = "utf-8" } + opts.ContentTypeCharset = strings.ToLower(charset) } - if charset != "" { - ctx.Resp.Header().Set("Content-Type", mimeType+"; charset="+strings.ToLower(charset)) - } else { - ctx.Resp.Header().Set("Content-Type", mimeType) - } - ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff") - isSVG := sniffedType.IsSvgImage() // serve types that can present a security risk with CSP @@ -109,16 +101,12 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") } - disposition := "inline" + opts.Disposition = "inline" if isSVG && !setting.UI.SVG.Enabled { - disposition = "attachment" + opts.Disposition = "attachment" } - // encode filename per https://datatracker.ietf.org/doc/html/rfc5987 - encodedFileName := `filename*=UTF-8''` + url.PathEscape(fileName) - - ctx.Resp.Header().Set("Content-Disposition", disposition+"; "+encodedFileName) - ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") + ctx.SetServeHeaders(opts) _, err = ctx.Resp.Write(buf) if err != nil { diff --git a/routers/init.go b/routers/init.go index 9045437f87..1f26cf42d4 100644 --- a/routers/init.go +++ b/routers/init.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + forgejo "code.gitea.io/gitea/routers/api/forgejo/v1" packages_router "code.gitea.io/gitea/routers/api/packages" apiv1 "code.gitea.io/gitea/routers/api/v1" "code.gitea.io/gitea/routers/common" @@ -76,21 +77,31 @@ func InitGitServices() { mustInit(repo_service.Init) } -func syncAppPathForGit(ctx context.Context) error { +func syncAppConfForGit(ctx context.Context) error { runtimeState := new(system.RuntimeState) if err := system.AppState.Get(runtimeState); err != nil { return err } + + updated := false if runtimeState.LastAppPath != setting.AppPath { log.Info("AppPath changed from '%s' to '%s'", runtimeState.LastAppPath, setting.AppPath) + runtimeState.LastAppPath = setting.AppPath + updated = true + } + if runtimeState.LastCustomConf != setting.CustomConf { + log.Info("CustomConf changed from '%s' to '%s'", runtimeState.LastCustomConf, setting.CustomConf) + runtimeState.LastCustomConf = setting.CustomConf + updated = true + } + if updated { log.Info("re-sync repository hooks ...") mustInitCtx(ctx, repo_service.SyncRepositoryHooks) log.Info("re-write ssh public keys ...") mustInit(asymkey_model.RewriteAllPublicKeys) - runtimeState.LastAppPath = setting.AppPath return system.AppState.Set(runtimeState) } return nil @@ -153,7 +164,7 @@ func GlobalInitInstalled(ctx context.Context) { mustInit(repo_migrations.Init) eventsource.GetManager().Init() - mustInitCtx(ctx, syncAppPathForGit) + mustInitCtx(ctx, syncAppConfForGit) mustInit(ssh.Init) @@ -174,6 +185,7 @@ func NormalRoutes(ctx context.Context) *web.Route { r.Mount("/", web_routers.Routes(ctx)) r.Mount("/api/v1", apiv1.Routes(ctx)) + r.Mount("/api/forgejo/v1", forgejo.Routes(ctx)) r.Mount("/api/internal", private.Routes()) if setting.Packages.Enabled { r.Mount("/api/packages", packages_router.Routes(ctx)) diff --git a/routers/install/install.go b/routers/install/install.go index 962dee8c86..d205102389 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/models/migrations" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/generate" @@ -80,7 +81,7 @@ func Init(ctx goctx.Context) func(next http.Handler) http.Handler { "AllLangs": translation.AllLangs(), "PageStartTime": startTime, - "PasswordHashAlgorithms": user_model.AvailableHashAlgorithms, + "PasswordHashAlgorithms": hash.RecommendedHashAlgorithms, }, } defer ctx.Close() @@ -149,8 +150,8 @@ func Install(ctx *context.Context) { // Server and other services settings form.OfflineMode = setting.OfflineMode - form.DisableGravatar = false // when installing, there is no database connection so that given a default value - form.EnableFederatedAvatar = false // when installing, there is no database connection so that given a default value + form.DisableGravatar = setting.DisableGravatar // when installing, there is no database connection so that given a default value + form.EnableFederatedAvatar = setting.EnableFederatedAvatar // when installing, there is no database connection so that given a default value form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp @@ -185,7 +186,7 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { if err = db.InitEngine(ctx); err != nil { if strings.Contains(err.Error(), `Unknown database type: sqlite3`) { ctx.Data["Err_DbType"] = true - ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.io/en-us/install-from-binary/"), tplInstall, form) + ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://forgejo.org/download#installation-from-binary"), tplInstall, form) } else { ctx.Data["Err_DbSetting"] = true ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) @@ -443,10 +444,13 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode)) // if you are reinstalling, this maybe not right because of missing version if err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, strconv.FormatBool(form.DisableGravatar)); err != nil { - ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) + ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + return + } + if err := system_model.SetSettingNoVersion(system_model.KeyPictureEnableFederatedAvatar, strconv.FormatBool(form.EnableFederatedAvatar)); err != nil { + ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } - cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp)) cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration)) @@ -473,12 +477,16 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("security").Key("INSTALL_LOCK").SetValue("true") - var internalToken string - if internalToken, err = generate.NewInternalToken(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form) - return + // the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty) + // if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN + if setting.InternalToken == "" { + var internalToken string + if internalToken, err = generate.NewInternalToken(); err != nil { + ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form) + return + } + cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken) } - cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken) // if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted if setting.SecretKey == "" { diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 93aa450f9c..614daddc39 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -174,13 +174,6 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { return } - if !repo.AllowsPulls() { - // We can stop there's no need to go any further - ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ - RepoWasEmpty: wasEmpty, - }) - return - } baseRepo = repo if repo.IsFork { @@ -192,7 +185,17 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { }) return } - baseRepo = repo.BaseRepo + if repo.BaseRepo.AllowsPulls() { + baseRepo = repo.BaseRepo + } + } + + if !baseRepo.AllowsPulls() { + // We can stop there's no need to go any further + ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ + RepoWasEmpty: wasEmpty, + }) + return } } @@ -218,14 +221,14 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) } results = append(results, private.HookPostReceiveBranchResult{ - Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(), + Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(), Create: true, Branch: branch, URL: fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)), }) } else { results = append(results, private.HookPostReceiveBranchResult{ - Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(), + Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(), Create: false, Branch: branch, URL: fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index), diff --git a/routers/private/mail.go b/routers/private/mail.go index e858992aee..d55e33fd23 100644 --- a/routers/private/mail.go +++ b/routers/private/mail.go @@ -81,7 +81,7 @@ func SendEmail(ctx *context.PrivateContext) { func sendEmail(ctx *context.PrivateContext, subject, message string, to []string) { for _, email := range to { - msg := mailer.NewMessage([]string{email}, subject, message) + msg := mailer.NewMessage(email, subject, message) mailer.SendAsync(msg) } diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index b79b317555..7ea8a52809 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -159,7 +159,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source { func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source { return &smtp.Source{ Auth: form.SMTPAuth, - Addr: form.SMTPAddr, + Host: form.SMTPHost, Port: form.SMTPPort, AllowedDomains: form.AllowedDomains, ForceSMTPS: form.ForceSMTPS, diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 614d3d4f66..21756154d8 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -6,9 +6,11 @@ package admin import ( + "fmt" "net/http" "net/url" "os" + "strconv" "strings" system_model "code.gitea.io/gitea/models/system" @@ -18,7 +20,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - system_module "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/mailer" @@ -203,7 +204,21 @@ func ChangeConfig(ctx *context.Context) { value := ctx.FormString("value") version := ctx.FormInt("version") - if err := system_module.SetSetting(key, value, version); err != nil { + if check, ok := changeConfigChecks[key]; ok { + if err := check(ctx, value); err != nil { + log.Warn("refused to set setting: %v", err) + ctx.JSON(http.StatusOK, map[string]string{ + "err": ctx.Tr("admin.config.set_setting_failed", key), + }) + return + } + } + + if err := system_model.SetSetting(&system_model.Setting{ + SettingKey: key, + SettingValue: value, + Version: version, + }); err != nil { log.Error("set setting failed: %v", err) ctx.JSON(http.StatusOK, map[string]string{ "err": ctx.Tr("admin.config.set_setting_failed", key), @@ -215,3 +230,18 @@ func ChangeConfig(ctx *context.Context) { "version": version + 1, }) } + +var changeConfigChecks = map[string]func(ctx *context.Context, newValue string) error{ + system_model.KeyPictureDisableGravatar: func(_ *context.Context, newValue string) error { + if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && !v { + return fmt.Errorf("%q should be true when OFFLINE_MODE is true", system_model.KeyPictureDisableGravatar) + } + return nil + }, + system_model.KeyPictureEnableFederatedAvatar: func(_ *context.Context, newValue string) error { + if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && v { + return fmt.Errorf("%q cannot be false when OFFLINE_MODE is true", system_model.KeyPictureEnableFederatedAvatar) + } + return nil + }, +} diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go index bf71cb5595..c966396def 100644 --- a/routers/web/admin/hooks.go +++ b/routers/web/admin/hooks.go @@ -23,6 +23,7 @@ const ( func DefaultOrSystemWebhooks(ctx *context.Context) { var err error + ctx.Data["Title"] = ctx.Tr("admin.hooks") ctx.Data["PageIsAdminSystemHooks"] = true ctx.Data["PageIsAdminDefaultHooks"] = true diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 5cdfb8142e..ef3645e478 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -15,10 +15,10 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 25d70d7c47..dad297925e 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -14,13 +14,13 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/hcaptcha" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/mcaptcha" - "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" @@ -783,6 +783,13 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) { return } + // Register last login + user.SetLastLogin() + if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil { + ctx.ServerError("UpdateUserCols", err) + return + } + ctx.Flash.Success(ctx.Tr("auth.account_activated")) ctx.Redirect(setting.AppSubURL + "/") } diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index c21ca9cf69..c5a41d24fe 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -10,10 +10,10 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index ea3d83e8d6..260cfc4efa 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -69,6 +69,10 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, orderBy = "`user`.updated_unix ASC" case "reversealphabetically": orderBy = "`user`.name DESC" + case "lastlogin": + orderBy = "`user`.last_login_unix ASC" + case "reverselastlogin": + orderBy = "`user`.last_login_unix DESC" case UserSearchDefaultSortType: // "alphabetically" default: orderBy = "`user`.name ASC" diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go index 0e11f210ce..ffa34572bc 100644 --- a/routers/web/feed/profile.go +++ b/routers/web/feed/profile.go @@ -5,7 +5,6 @@ package feed import ( - "net/http" "time" activities_model "code.gitea.io/gitea/models/activities" @@ -59,7 +58,6 @@ func showUserFeed(ctx *context.Context, formatType string) { // writeFeed write a feeds.Feed as atom or rss to ctx.Resp func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) { - ctx.Resp.WriteHeader(http.StatusOK) if formatType == "atom" { ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8") if err := feed.WriteAtom(ctx.Resp); err != nil { diff --git a/routers/web/misc/swagger-forgejo.go b/routers/web/misc/swagger-forgejo.go new file mode 100644 index 0000000000..2f539e955c --- /dev/null +++ b/routers/web/misc/swagger-forgejo.go @@ -0,0 +1,19 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package misc + +import ( + "net/http" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" +) + +// tplSwagger swagger page template +const tplForgejoSwagger base.TplName = "swagger/forgejo-ui" + +func SwaggerForgejo(ctx *context.Context) { + ctx.Data["APIVersion"] = "v1" + ctx.HTML(http.StatusOK, tplForgejoSwagger) +} diff --git a/routers/web/org/members.go b/routers/web/org/members.go index ec5a98fc6a..6aeff57071 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -108,13 +108,20 @@ func MembersAction(ctx *context.Context) { } case "leave": err = models.RemoveOrgUser(org.ID, ctx.Doer.ID) - if organization.IsErrLastOrgOwner(err) { + if err == nil { + ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName())) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": "", // keep the user stay on current page, in case they want to do other operations. + }) + } else if organization.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) ctx.JSON(http.StatusOK, map[string]interface{}{ "redirect": ctx.Org.OrgLink + "/members", }) - return + } else { + log.Error("RemoveOrgUser(%d,%d): %v", org.ID, ctx.Doer.ID, err) } + return } if err != nil { diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index d1f1255db4..6085fd8693 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -126,6 +126,10 @@ func RestoreBranchPost(ctx *context.Context) { log.Error("GetDeletedBranchByID: %v", err) ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName)) return + } else if deletedBranch == nil { + log.Debug("RestoreBranch: Can't restore branch[%d] '%s', as it does not exist", branchID, branchName) + ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName)) + return } if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{ diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index a6553256b0..b56cf928cd 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -284,7 +284,7 @@ func Diff(ctx *context.Context) { } return } - if len(commitID) != 40 { + if len(commitID) != git.SHAFullLength { commitID = commit.ID.String() } diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index db6b59471f..1b8521dd92 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -43,8 +43,8 @@ const ( ) // setCompareContext sets context data. -func setCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) { - ctx.Data["BaseCommit"] = base +func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner, headName string) { + ctx.Data["BeforeCommit"] = before ctx.Data["HeadCommit"] = head ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob { @@ -59,7 +59,7 @@ func setCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, return blob } - setPathsCompareContext(ctx, base, head, headOwner, headName) + setPathsCompareContext(ctx, before, head, headOwner, headName) setImageCompareContext(ctx) setCsvCompareContext(ctx) } @@ -629,9 +629,8 @@ func PrepareCompareDiff( } baseGitRepo := ctx.Repo.GitRepo - baseCommitID := ci.CompareInfo.BaseCommitID - baseCommit, err := baseGitRepo.GetCommit(baseCommitID) + beforeCommit, err := baseGitRepo.GetCommit(beforeCommitID) if err != nil { ctx.ServerError("GetCommit", err) return false @@ -668,7 +667,7 @@ func PrepareCompareDiff( ctx.Data["Username"] = ci.HeadUser.Name ctx.Data["Reponame"] = ci.HeadRepo.Name - setCompareContext(ctx, baseCommit, headCommit, ci.HeadUser.Name, repo.Name) + setCompareContext(ctx, beforeCommit, headCommit, ci.HeadUser.Name, repo.Name) return false } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 38ad593c17..a64d8425df 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -784,6 +784,11 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles } } } + + } + + if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/ + template.Ref = git.BranchPrefix + template.Ref } ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0 ctx.Data["label_ids"] = strings.Join(labelIDs, ",") @@ -1113,7 +1118,11 @@ func NewIssuePost(ctx *context.Context) { } // roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue -func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue) (issues_model.RoleDescriptor, error) { +func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { + if hasOriginalAuthor { + return issues_model.RoleDescriptorNone, nil + } + perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) if err != nil { return issues_model.RoleDescriptorNone, err @@ -1415,7 +1424,7 @@ func ViewIssue(ctx *context.Context) { // check if dependencies can be created across repositories ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies - if issue.ShowRole, err = roleDescriptor(ctx, repo, issue.Poster, issue); err != nil { + if issue.ShowRole, err = roleDescriptor(ctx, repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil { ctx.ServerError("roleDescriptor", err) return } @@ -1454,7 +1463,7 @@ func ViewIssue(ctx *context.Context) { continue } - comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue) + comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue, comment.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return @@ -1553,7 +1562,7 @@ func ViewIssue(ctx *context.Context) { continue } - c.ShowRole, err = roleDescriptor(ctx, repo, c.Poster, issue) + c.ShowRole, err = roleDescriptor(ctx, repo, c.Poster, issue, c.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go index a9386d274a..7d188df4a0 100644 --- a/routers/web/repo/issue_content_history.go +++ b/routers/web/repo/issue_content_history.go @@ -6,16 +6,17 @@ package repo import ( "bytes" - "fmt" "html" "net/http" "strings" + "code.gitea.io/gitea/models/avatars" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "github.com/sergi/go-diff/diffmatchpatch" @@ -64,16 +65,20 @@ func GetContentHistoryList(ctx *context.Context) { } else { actionText = ctx.Locale.Tr("repo.issues.content_history.edited") } - timeSinceText := timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale) username := item.UserName if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" { username = strings.TrimSpace(item.UserFullName) } + src := html.EscapeString(item.UserAvatarLink) + class := avatars.DefaultAvatarClass + " mr-3" + name := html.EscapeString(username) + avatarHTML := string(templates.AvatarHTML(src, 28, class, username)) + timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale)) + results = append(results, map[string]interface{}{ - "name": fmt.Sprintf("%s %s %s", - html.EscapeString(item.UserAvatarLink), html.EscapeString(username), actionText, timeSinceText), + "name": avatarHTML + "" + name + " " + actionText + " " + timeSinceText, "value": item.HistoryID, }) } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index fc95bbf240..98b78645dc 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1381,7 +1381,7 @@ func CleanUpPullRequest(ctx *context.Context) { } func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) { - fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch + fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch if err := repo_service.DeleteBranch(ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil { switch { case git.IsErrBranchNotExist(err): diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 3e746d3f05..0bad3f5de1 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -426,7 +426,10 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep } defer fr.Close() - ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime()) + ctx.ServeContent(fr, &context.ServeHeaderOptions{ + Filename: downloadName, + LastModified: archiver.CreatedUnix.AsLocalTime(), + }) } // InitiateDownload will enqueue an archival request, as needed. It may submit diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 2b5691ce88..3cf4c31db7 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -62,7 +62,7 @@ const ( // SettingsCtxData is a middleware that sets all the general context data for the // settings template. func SettingsCtxData(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.options") ctx.Data["PageIsSettingsOptions"] = true ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled @@ -854,7 +854,7 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R // Collaboration render a repository's collaboration page func Collaboration(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration") ctx.Data["PageIsSettingsCollaboration"] = true users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) @@ -1093,7 +1093,7 @@ func GitHooksEditPost(ctx *context.Context) { // DeployKeys render the deploy keys list of a repository page func DeployKeys(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + " / " + ctx.Tr("secrets.secrets") ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go index c4cd3486aa..55dcd12f73 100644 --- a/routers/web/repo/setting_protected_branch.go +++ b/routers/web/repo/setting_protected_branch.go @@ -29,7 +29,7 @@ import ( // ProtectedBranch render the page to protect the repository func ProtectedBranch(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.branches") ctx.Data["PageIsSettingsBranches"] = true protectedBranches, err := git_model.GetProtectedBranches(ctx.Repo.Repository.ID) @@ -61,7 +61,7 @@ func ProtectedBranch(ctx *context.Context) { // ProtectedBranchPost response for protect for a branch of a repository func ProtectedBranchPost(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.protected_branch") ctx.Data["PageIsSettingsBranches"] = true repo := ctx.Repo.Repository diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go index f63a50782b..2c24d3fdf1 100644 --- a/routers/web/repo/tag.go +++ b/routers/web/repo/tag.go @@ -134,7 +134,7 @@ func DeleteProtectedTagPost(ctx *context.Context) { } func setTagsContext(ctx *context.Context) error { - ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["Title"] = ctx.Tr("repo.settings.tags") ctx.Data["PageIsSettingsTags"] = true protectedTags, err := git_model.GetProtectedTags(ctx.Repo.Repository.ID) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index d35ec48df0..293a957266 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -10,7 +10,6 @@ import ( gocontext "context" "encoding/base64" "fmt" - gotemplate "html/template" "io" "net/http" "net/url" @@ -342,15 +341,13 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin if err != nil { log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.name, ctx.Repo.Repository, err) buf := &bytes.Buffer{} - ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, buf, ctx.Locale) - ctx.Data["FileContent"] = strings.ReplaceAll( - gotemplate.HTMLEscapeString(buf.String()), "\n", `
`, - ) + ctx.Data["EscapeStatus"], _ = charset.EscapeControlStringReader(rd, buf, ctx.Locale) + ctx.Data["FileContent"] = buf.String() } } else { - ctx.Data["IsRenderedHTML"] = true + ctx.Data["IsPlainText"] = true buf := &bytes.Buffer{} - ctx.Data["EscapeStatus"], err = charset.EscapeControlReader(rd, &charset.BreakWriter{Writer: buf}, ctx.Locale, charset.RuneNBSP) + ctx.Data["EscapeStatus"], err = charset.EscapeControlStringReader(rd, buf, ctx.Locale) if err != nil { log.Error("Read failed: %v", err) } @@ -522,15 +519,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st } // to prevent iframe load third-party url ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") - } else if readmeExist && !shouldRenderSource { - buf := &bytes.Buffer{} - ctx.Data["IsRenderedHTML"] = true - - ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, buf, ctx.Locale) - - ctx.Data["FileContent"] = strings.ReplaceAll( - gotemplate.HTMLEscapeString(buf.String()), "\n", `
`, - ) } else { buf, _ := io.ReadAll(rd) diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index ee980333b7..f37454a42c 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -297,6 +297,34 @@ func editWebhook(ctx *context.Context, params webhookParams) { ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID)) } +// ForgejoHooksNewPost response for creating Forgejo webhook +func ForgejoHooksNewPost(ctx *context.Context) { + createWebhook(ctx, forgejoHookParams(ctx)) +} + +// ForgejoHooksEditPost response for editing Forgejo webhook +func ForgejoHooksEditPost(ctx *context.Context) { + editWebhook(ctx, forgejoHookParams(ctx)) +} + +func forgejoHookParams(ctx *context.Context) webhookParams { + form := web.GetForm(ctx).(*forms.NewWebhookForm) + + contentType := webhook.ContentTypeJSON + if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm { + contentType = webhook.ContentTypeForm + } + + return webhookParams{ + Type: webhook.FORGEJO, + URL: form.PayloadURL, + ContentType: contentType, + Secret: form.Secret, + HTTPMethod: form.HTTPMethod, + WebhookForm: form.WebhookForm, + } +} + // GiteaHooksNewPost response for creating Gitea webhook func GiteaHooksNewPost(ctx *context.Context) { createWebhook(ctx, giteaHookParams(ctx)) diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 89bd23588b..62f3f315e6 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -27,7 +27,7 @@ func CodeSearch(ctx *context.Context) { ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - ctx.Data["Title"] = ctx.Tr("code.title") + ctx.Data["Title"] = ctx.Tr("explore.code") ctx.Data["ContextUser"] = ctx.ContextUser language := ctx.FormTrim("l") diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 7179e2df97..7be37b6a50 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -402,5 +402,8 @@ func DownloadPackageFile(ctx *context.Context) { } defer s.Close() - ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) + ctx.ServeContent(s, &context.ServeHeaderOptions{ + Filename: pf.Name, + LastModified: pf.CreatedUnix.AsLocalTime(), + }) } diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 8b95caf2fc..30da9d11d5 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -12,10 +12,10 @@ import ( "code.gitea.io/gitea/models" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/auth/password" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" @@ -31,7 +31,7 @@ const ( // Account renders change user's password, user's email and user suicide page func Account(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.account") ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.Doer.Email ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go index a92aa6e989..0df9317870 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -18,7 +18,7 @@ import ( // AdoptOrDeleteRepository adopts or deletes a repository func AdoptOrDeleteRepository(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.adopt") ctx.Data["PageIsSettingsRepos"] = true allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowAdopt"] = allowAdopt diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index e9572a07a6..2649669bb2 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -22,7 +22,7 @@ const ( // Applications render manage access token page func Applications(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.applications") ctx.Data["PageIsSettingsApplications"] = true loadApplicationsData(ctx) diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index a8d07ea47a..5d69eb7b86 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -24,7 +24,7 @@ const ( // Keys render user's SSH/GPG public keys page func Keys(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys") ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer @@ -100,14 +100,18 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Content"] = true ctx.Data["Err_Signature"] = true - ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGInvalidTokenSignature).ID + keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID + ctx.Data["KeyID"] = keyID + ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) case asymkey_model.IsErrGPGNoEmailFound(err): loadKeysData(ctx) ctx.Data["Err_Content"] = true ctx.Data["Err_Signature"] = true - ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGNoEmailFound).ID + keyID := err.(asymkey_model.ErrGPGNoEmailFound).ID + ctx.Data["KeyID"] = keyID + ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) default: ctx.ServerError("AddPublicKey", err) @@ -139,7 +143,9 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["VerifyingID"] = form.KeyID ctx.Data["Err_Signature"] = true - ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGInvalidTokenSignature).ID + keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID + ctx.Data["KeyID"] = keyID + ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) default: ctx.ServerError("VerifyGPG", err) diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index ba3f5b5080..a710773f38 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -43,7 +43,7 @@ const ( // Profile render user's profile page func Profile(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.profile") ctx.Data["PageIsSettingsProfile"] = true ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() @@ -220,7 +220,7 @@ func DeleteAvatar(ctx *context.Context) { // Organization render all the organization of the user func Organization(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.organization") ctx.Data["PageIsSettingsOrganization"] = true opts := organization.FindOrgOptions{ @@ -255,7 +255,7 @@ func Organization(ctx *context.Context) { // Repos display a list of all repositories of the user func Repos(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.repos") ctx.Data["PageIsSettingsRepos"] = true ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories @@ -361,7 +361,7 @@ func Repos(ctx *context.Context) { // Appearance render user's appearance settings func Appearance(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.appearance") ctx.Data["PageIsSettingsAppearance"] = true var hiddenCommentTypes *big.Int diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go index 57ea24eeb1..9cb4427a1b 100644 --- a/routers/web/user/setting/security/security.go +++ b/routers/web/user/setting/security/security.go @@ -23,7 +23,7 @@ const ( // Security render change user's password page and 2FA func Security(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["Title"] = ctx.Tr("settings.security") ctx.Data["PageIsSettingsSecurity"] = true if ctx.FormString("openid.return_to") != "" { diff --git a/routers/web/web.go b/routers/web/web.go index 9b814c3f54..3f41592a17 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -211,6 +211,7 @@ func Routes(ctx gocontext.Context) *web.Route { if setting.API.EnableSwagger { // Note: The route moved from apiroutes because it's in fact want to render a web page routes.Get("/api/swagger", append(common, misc.Swagger)...) // Render V1 by default + routes.Get("/api/forgejo/swagger", append(common, misc.SwaggerForgejo)...) } // TODO: These really seem like things that could be folded into Contexter or as helper functions @@ -533,6 +534,7 @@ func RegisterRoutes(m *web.Route) { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) + m.Post("/forgejo/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksEditPost) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) @@ -548,6 +550,7 @@ func RegisterRoutes(m *web.Route) { m.Group("/{configType:default-hooks|system-hooks}", func() { m.Get("/{type}/new", repo.WebhooksNew) + m.Post("/forgejo/new", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksNewPost) m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) @@ -597,7 +600,10 @@ func RegisterRoutes(m *web.Route) { m.Group("", func() { m.Get("/favicon.ico", func(ctx *context.Context) { - ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png")) + ctx.SetServeHeaders(&context.ServeHeaderOptions{ + Filename: "favicon.png", + }) + http.ServeFile(ctx.Resp, ctx.Req, path.Join(setting.StaticRootPath, "public/img/favicon.png")) }) m.Group("/{username}", func() { m.Get(".png", func(ctx *context.Context) { ctx.Error(http.StatusNotFound) }) @@ -623,7 +629,6 @@ func RegisterRoutes(m *web.Route) { reqRepoReleaseWriter := context.RequireRepoWriter(unit.TypeReleases) reqRepoReleaseReader := context.RequireRepoReader(unit.TypeReleases) reqRepoWikiWriter := context.RequireRepoWriter(unit.TypeWiki) - reqRepoIssueWriter := context.RequireRepoWriter(unit.TypeIssues) reqRepoIssueReader := context.RequireRepoReader(unit.TypeIssues) reqRepoPullsReader := context.RequireRepoReader(unit.TypePullRequests) reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(unit.TypeIssues, unit.TypePullRequests) @@ -709,6 +714,7 @@ func RegisterRoutes(m *web.Route) { m.Get("", org.Webhooks) m.Post("/delete", org.DeleteWebhook) m.Get("/{type}/new", repo.WebhooksNew) + m.Post("/forgejo/new", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksNewPost) m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) @@ -723,6 +729,7 @@ func RegisterRoutes(m *web.Route) { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) + m.Post("/forgejo/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksEditPost) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) @@ -832,6 +839,7 @@ func RegisterRoutes(m *web.Route) { m.Get("", repo.Webhooks) m.Post("/delete", repo.DeleteWebhook) m.Get("/{type}/new", repo.WebhooksNew) + m.Post("/forgejo/new", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksNewPost) m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost) m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) @@ -848,6 +856,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/test", repo.TestWebhook) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) + m.Post("/forgejo/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.ForgejoHooksEditPost) m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) @@ -944,8 +953,8 @@ func RegisterRoutes(m *web.Route) { }) }) m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeIssueReaction) - m.Post("/lock", reqRepoIssueWriter, bindIgnErr(forms.IssueLockForm{}), repo.LockIssue) - m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue) + m.Post("/lock", reqRepoIssuesOrPullsWriter, bindIgnErr(forms.IssueLockForm{}), repo.LockIssue) + m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue) m.Post("/delete", reqRepoAdmin, repo.DeleteIssue) }, context.RepoMustNotBeArchived()) m.Group("/{index}", func() { diff --git a/services/auth/basic.go b/services/auth/basic.go index 9b32ad29af..a6294dbee0 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -41,20 +41,20 @@ func (b *Basic) Name() string { // "Authorization" header of the request and returns the corresponding user object for that // name/token on successful validation. // Returns nil if header is empty or validation fails. -func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User { +func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { // Basic authentication should only fire on API, Download or on Git or LFSPaths if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) { - return nil + return nil, nil } baHead := req.Header.Get("Authorization") if len(baHead) == 0 { - return nil + return nil, nil } auths := strings.SplitN(baHead, " ", 2) if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") { - return nil + return nil, nil } uname, passwd, _ := base.BasicAuthDecode(auths[1]) @@ -78,11 +78,11 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore u, err := user_model.GetUserByID(uid) if err != nil { log.Error("GetUserByID: %v", err) - return nil + return nil, err } store.GetData()["IsApiToken"] = true - return u + return u, nil } token, err := auth_model.GetAccessTokenBySHA(authToken) @@ -91,7 +91,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore u, err := user_model.GetUserByID(token.UID) if err != nil { log.Error("GetUserByID: %v", err) - return nil + return nil, err } token.UpdatedUnix = timeutil.TimeStampNow() @@ -100,13 +100,13 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore } store.GetData()["IsApiToken"] = true - return u + return u, nil } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { log.Error("GetAccessTokenBySha: %v", err) } if !setting.Service.EnableBasicAuth { - return nil + return nil, nil } log.Trace("Basic Authorization: Attempting SignIn for %s", uname) @@ -115,7 +115,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore if !user_model.IsErrUserNotExist(err) { log.Error("UserSignIn: %v", err) } - return nil + return nil, err } if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() { @@ -124,5 +124,5 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore log.Trace("Basic Authorization: Logged in user %-v", u) - return u + return u, nil } diff --git a/services/auth/group.go b/services/auth/group.go index bbafe64b49..ef68fc35a6 100644 --- a/services/auth/group.go +++ b/services/auth/group.go @@ -10,7 +10,6 @@ import ( "reflect" "strings" - "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" ) @@ -81,23 +80,23 @@ func (b *Group) Free() error { } // Verify extracts and validates -func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User { - if !db.HasEngine { - return nil - } - +func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { // Try to sign in with each of the enabled plugins for _, ssoMethod := range b.methods { - user := ssoMethod.Verify(req, w, store, sess) + user, err := ssoMethod.Verify(req, w, store, sess) + if err != nil { + return nil, err + } + if user != nil { if store.GetData()["AuthedMethod"] == nil { if named, ok := ssoMethod.(Named); ok { store.GetData()["AuthedMethod"] = named.Name() } } - return user + return user, nil } } - return nil + return nil, nil } diff --git a/services/auth/httpsign.go b/services/auth/httpsign.go index 67053d2b77..4e01bc29c6 100644 --- a/services/auth/httpsign.go +++ b/services/auth/httpsign.go @@ -40,10 +40,10 @@ func (h *HTTPSign) Name() string { // Verify extracts and validates HTTPsign from the Signature header of the request and returns // the corresponding user object on successful validation. // Returns nil if header is empty or validation fails. -func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User { +func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { sigHead := req.Header.Get("Signature") if len(sigHead) == 0 { - return nil + return nil, nil } var ( @@ -54,14 +54,14 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt if len(req.Header.Get("X-Ssh-Certificate")) != 0 { // Handle Signature signed by SSH certificates if len(setting.SSH.TrustedUserCAKeys) == 0 { - return nil + return nil, nil } publicKey, err = VerifyCert(req) if err != nil { log.Debug("VerifyCert on request from %s: failed: %v", req.RemoteAddr, err) log.Warn("Failed authentication attempt from %s", req.RemoteAddr) - return nil + return nil, nil } } else { // Handle Signature signed by Public Key @@ -69,21 +69,21 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt if err != nil { log.Debug("VerifyPubKey on request from %s: failed: %v", req.RemoteAddr, err) log.Warn("Failed authentication attempt from %s", req.RemoteAddr) - return nil + return nil, nil } } u, err := user_model.GetUserByID(publicKey.OwnerID) if err != nil { log.Error("GetUserByID: %v", err) - return nil + return nil, err } store.GetData()["IsApiToken"] = true log.Trace("HTTP Sign: Logged in user %-v", u) - return u + return u, nil } func VerifyPubKey(r *http.Request) (*asymkey_model.PublicKey, error) { diff --git a/services/auth/interface.go b/services/auth/interface.go index ecc9ad2ca6..18d2eadb52 100644 --- a/services/auth/interface.go +++ b/services/auth/interface.go @@ -25,8 +25,9 @@ type Method interface { // If verification is successful returns either an existing user object (with id > 0) // or a new user object (with id = 0) populated with the information that was found // in the authentication data (username or email). - // Returns nil if verification fails. - Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User + // Second argument returns err if verification fails, otherwise + // First return argument returns nil if no matched verification condition + Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) } // Initializable represents a structure that requires initialization diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 8f038d6104..2f1154ebbe 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -109,18 +109,14 @@ func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 { // or the "Authorization" header and returns the corresponding user object for that ID. // If verification is successful returns an existing user object. // Returns nil if verification fails. -func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User { - if !db.HasEngine { - return nil - } - +func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) { - return nil + return nil, nil } id := o.userIDFromToken(req, store) if id <= 0 { - return nil + return nil, nil } log.Trace("OAuth2 Authorization: Found token for user[%d]", id) @@ -129,11 +125,11 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor if !user_model.IsErrUserNotExist(err) { log.Error("GetUserByName: %v", err) } - return nil + return nil, err } log.Trace("OAuth2 Authorization: Logged in user %-v", user) - return user + return user, nil } func isAuthenticatedTokenRequest(req *http.Request) bool { diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go index 8dec1c8ea7..59cd1fe05d 100644 --- a/services/auth/reverseproxy.go +++ b/services/auth/reverseproxy.go @@ -52,10 +52,10 @@ func (r *ReverseProxy) Name() string { // If a username is available in the "setting.ReverseProxyAuthUser" header an existing // user object is returned (populated with username or email found in header). // Returns nil if header is empty. -func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) *user_model.User { +func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) (*user_model.User, error) { username := r.getUserName(req) if len(username) == 0 { - return nil + return nil, nil } log.Trace("ReverseProxy Authorization: Found username: %s", username) @@ -63,11 +63,11 @@ func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) *user_model.User { if err != nil { if !user_model.IsErrUserNotExist(err) || !r.isAutoRegisterAllowed() { log.Error("GetUserByName: %v", err) - return nil + return nil, err } user = r.newUser(req) } - return user + return user, nil } // getEmail extracts the email from the "setting.ReverseProxyAuthEmail" header @@ -107,12 +107,15 @@ func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) *user_model.User // First it will attempt to load it based on the username (see docs for getUserFromAuthUser), // and failing that it will attempt to load it based on the email (see docs for getUserFromAuthEmail). // Returns nil if the headers are empty or the user is not found. -func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User { - user := r.getUserFromAuthUser(req) +func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { + user, err := r.getUserFromAuthUser(req) + if err != nil { + return nil, err + } if user == nil { user = r.getUserFromAuthEmail(req) if user == nil { - return nil + return nil, nil } } @@ -125,7 +128,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da store.GetData()["IsReverseProxy"] = true log.Trace("ReverseProxy Authorization: Logged in user %-v", user) - return user + return user, nil } // isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true diff --git a/services/auth/session.go b/services/auth/session.go index 1ec94aa0af..adc4be5399 100644 --- a/services/auth/session.go +++ b/services/auth/session.go @@ -29,12 +29,12 @@ func (s *Session) Name() string { // Verify checks if there is a user uid stored in the session and returns the user // object for that uid. // Returns nil if there is no user uid stored in the session. -func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User { +func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { user := SessionUser(sess) if user != nil { - return user + return user, nil } - return nil + return nil, nil } // SessionUser returns the user object corresponding to the "uid" session variable. diff --git a/services/auth/source/smtp/auth.go b/services/auth/source/smtp/auth.go index 487c049722..e8453fde69 100644 --- a/services/auth/source/smtp/auth.go +++ b/services/auth/source/smtp/auth.go @@ -58,10 +58,10 @@ var ErrUnsupportedLoginType = errors.New("Login source is unknown") func Authenticate(a smtp.Auth, source *Source) error { tlsConfig := &tls.Config{ InsecureSkipVerify: source.SkipVerify, - ServerName: source.Addr, + ServerName: source.Host, } - conn, err := net.Dial("tcp", net.JoinHostPort(source.Addr, strconv.Itoa(source.Port))) + conn, err := net.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port))) if err != nil { return err } @@ -71,7 +71,7 @@ func Authenticate(a smtp.Auth, source *Source) error { conn = tls.Client(conn, tlsConfig) } - client, err := smtp.NewClient(conn, source.Addr) + client, err := smtp.NewClient(conn, source.Host) if err != nil { return fmt.Errorf("failed to create NewClient: %w", err) } diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go index b2286d42a0..5e69f912da 100644 --- a/services/auth/source/smtp/source.go +++ b/services/auth/source/smtp/source.go @@ -19,7 +19,7 @@ import ( // Source holds configuration for the SMTP login source. type Source struct { Auth string - Addr string + Host string Port int AllowedDomains string `xorm:"TEXT"` ForceSMTPS bool diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go index 63fd3e5511..dff24d494e 100644 --- a/services/auth/source/smtp/source_authenticate.go +++ b/services/auth/source/smtp/source_authenticate.go @@ -32,7 +32,7 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str var auth smtp.Auth switch source.Auth { case PlainAuthentication: - auth = smtp.PlainAuth("", userName, password, source.Addr) + auth = smtp.PlainAuth("", userName, password, source.Host) case LoginAuthentication: auth = &loginAuthenticator{userName, password} case CRAMMD5Authentication: diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go index 757d596c4c..4bcee1203a 100644 --- a/services/auth/sspi_windows.go +++ b/services/auth/sspi_windows.go @@ -78,15 +78,15 @@ func (s *SSPI) Free() error { // If authentication is successful, returns the corresponding user object. // If negotiation should continue or authentication fails, immediately returns a 401 HTTP // response code, as required by the SPNEGO protocol. -func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User { +func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { if !s.shouldAuthenticate(req) { - return nil + return nil, nil } cfg, err := s.getConfig() if err != nil { log.Error("could not get SSPI config: %v", err) - return nil + return nil, err } log.Trace("SSPI Authorization: Attempting to authenticate") @@ -109,7 +109,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, log.Error("%v", err) } - return nil + return nil, err } if outToken != "" { sspiAuth.AppendAuthenticateHeader(w, outToken) @@ -117,7 +117,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, username := sanitizeUsername(userInfo.Username, cfg) if len(username) == 0 { - return nil + return nil, nil } log.Info("Authenticated as %s\n", username) @@ -125,16 +125,16 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, if err != nil { if !user_model.IsErrUserNotExist(err) { log.Error("GetUserByName: %v", err) - return nil + return nil, err } if !cfg.AutoCreateUsers { log.Error("User '%s' not found", username) - return nil + return nil, nil } user, err = s.newUser(username, cfg) if err != nil { log.Error("CreateUser: %v", err) - return nil + return nil, err } } @@ -144,7 +144,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, } log.Trace("SSPI Authorization: Logged in user %-v", user) - return user + return user, nil } // getConfig retrieves the SSPI configuration from login sources diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index c730477cbd..82137638b1 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -147,18 +147,20 @@ func registerDeleteOldActions() { func registerUpdateGiteaChecker() { type UpdateCheckerConfig struct { BaseConfig - HTTPEndpoint string + HTTPEndpoint string + DomainEndpoint string } RegisterTaskFatal("update_checker", &UpdateCheckerConfig{ BaseConfig: BaseConfig{ - Enabled: true, + Enabled: false, RunAtStart: false, Schedule: "@every 168h", }, - HTTPEndpoint: "https://dl.gitea.io/gitea/version.json", + HTTPEndpoint: "https://dl.gitea.io/gitea/version.json", + DomainEndpoint: "release.forgejo.org", }, func(ctx context.Context, _ *user_model.User, config Config) error { updateCheckerConfig := config.(*UpdateCheckerConfig) - return updatechecker.GiteaUpdateChecker(updateCheckerConfig.HTTPEndpoint) + return updatechecker.GiteaUpdateChecker(updateCheckerConfig.HTTPEndpoint, updateCheckerConfig.DomainEndpoint) }) } diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index 9064be2cca..7e7c756752 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -45,7 +45,7 @@ type AuthenticationForm struct { IsActive bool IsSyncEnabled bool SMTPAuth string - SMTPAddr string + SMTPHost string SMTPPort int AllowedDomains string SecurityProtocol int `binding:"Range(0,2)"` diff --git a/services/issue/commit.go b/services/issue/commit.go index c8cfa6cc8a..b17f0e952c 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -19,6 +19,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/repository" @@ -176,7 +177,8 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm if !repo.CloseIssuesViaCommitInAnyBranch { // If the issue was specified to be in a particular branch, don't allow commits in other branches to close it if refIssue.Ref != "" { - if branchName != refIssue.Ref { + issueBranchName := strings.TrimPrefix(refIssue.Ref, git.BranchPrefix) + if branchName != issueBranchName { continue } // Otherwise, only process commits to the default branch diff --git a/services/issue/issue.go b/services/issue/issue.go index 47782e50d3..c522c0083f 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/modules/util" ) // NewIssue creates new issue with labels for repository. @@ -201,7 +200,7 @@ func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[i for _, issue := range issues { if issue.Ref != "" { issueRefEndNames[issue.ID] = git.RefEndName(issue.Ref) - issueRefURLs[issue.ID] = git.RefURL(repoLink, util.PathEscapeSegments(issue.Ref)) + issueRefURLs[issue.ID] = git.RefURL(repoLink, issue.Ref) } } return issueRefEndNames, issueRefURLs @@ -220,9 +219,16 @@ func deleteIssue(issue *issues_model.Issue) error { return err } - if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, issue.IsClosed); err != nil { + // update the total issue numbers + if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil { return err } + // if the issue is closed, update the closed issue numbers + if issue.IsClosed { + if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { + return err + } + } if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { return fmt.Errorf("error updating counters for milestone id %d: %w", diff --git a/services/mailer/mail.go b/services/mailer/mail.go index a5bfa496f9..23de3dc531 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -61,7 +61,7 @@ func SendTestMail(email string) error { // No mail service configured return nil } - return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) + return gomail.Send(Sender, NewMessage(email, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) } // sendUserMail sends a mail to the user @@ -86,7 +86,7 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s return } - msg := NewMessage([]string{u.Email}, subject, content.String()) + msg := NewMessage(u.Email, subject, content.String()) msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info) SendAsync(msg) @@ -137,7 +137,7 @@ func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) { return } - msg := NewMessage([]string{email.Email}, locale.Tr("mail.activate_email"), content.String()) + msg := NewMessage(email.Email, locale.Tr("mail.activate_email"), content.String()) msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID) SendAsync(msg) @@ -168,7 +168,7 @@ func SendRegisterNotifyMail(u *user_model.User) { return } - msg := NewMessage([]string{u.Email}, locale.Tr("mail.register_notify"), content.String()) + msg := NewMessage(u.Email, locale.Tr("mail.register_notify"), content.String()) msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID) SendAsync(msg) @@ -202,7 +202,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository) return } - msg := NewMessage([]string{u.Email}, subject, content.String()) + msg := NewMessage(u.Email, subject, content.String()) msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID) SendAsync(msg) @@ -306,7 +306,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient msgs := make([]*Message, 0, len(recipients)) for _, recipient := range recipients { - msg := NewMessageFrom([]string{recipient.Email}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String()) + msg := NewMessageFrom(recipient.Email, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String()) msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info) msg.SetHeader("Message-ID", "<"+msgID+">") @@ -373,6 +373,16 @@ func generateAdditionalHeaders(ctx *mailCommentContext, reason string, recipient "X-Gitea-Issue-ID": strconv.FormatInt(ctx.Issue.Index, 10), "X-Gitea-Issue-Link": ctx.Issue.HTMLURL(), + "X-Forgejo-Reason": reason, + "X-Forgejo-Sender": ctx.Doer.DisplayName(), + "X-Forgejo-Recipient": recipient.DisplayName(), + "X-Forgejo-Recipient-Address": recipient.Email, + "X-Forgejo-Repository": repo.Name, + "X-Forgejo-Repository-Path": repo.FullName(), + "X-Forgejo-Repository-Link": repo.HTMLURL(), + "X-Forgejo-Issue-ID": strconv.FormatInt(ctx.Issue.Index, 10), + "X-Forgejo-Issue-Link": ctx.Issue.HTMLURL(), + "X-GitHub-Reason": reason, "X-GitHub-Sender": ctx.Doer.DisplayName(), "X-GitHub-Recipient": recipient.DisplayName(), diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index 6df3fbbf1d..5e8edf7ad1 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -90,7 +90,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo publisherName := rel.Publisher.DisplayName() relURL := "<" + rel.HTMLURL() + ">" for _, to := range tos { - msg := NewMessageFrom([]string{to}, publisherName, setting.MailService.FromEmail, subject, mailBody.String()) + msg := NewMessageFrom(to, publisherName, setting.MailService.FromEmail, subject, mailBody.String()) msg.Info = subject msg.SetHeader("Message-ID", relURL) msgs = append(msgs, msg) diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go index 6fe9df0926..f8f443febd 100644 --- a/services/mailer/mail_repo.go +++ b/services/mailer/mail_repo.go @@ -83,9 +83,12 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U return err } - msg := NewMessage(emails, subject, content.String()) - msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID) + for _, to := range emails { + msg := NewMessage(to, subject, content.String()) + msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID) + + SendAsync(msg) + } - SendAsync(msg) return nil } diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index c2b2a00e76..45963a6660 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -53,7 +53,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod return err } - msg := NewMessage([]string{invite.Email}, subject, mailBody.String()) + msg := NewMessage(invite.Email, subject, mailBody.String()) msg.Info = subject SendAsync(msg) diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index 46b0c8e2f4..76106d30e1 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -36,7 +36,7 @@ type Message struct { Info string // Message information for log purpose. FromAddress string FromDisplayName string - To []string + To string // Use only one recipient to prevent leaking of addresses Subject string Date time.Time Body string @@ -47,7 +47,7 @@ type Message struct { func (m *Message) ToMessage() *gomail.Message { msg := gomail.NewMessage() msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName) - msg.SetHeader("To", m.To...) + msg.SetHeader("To", m.To) for header := range m.Headers { msg.SetHeader(header, m.Headers[header]...) } @@ -86,7 +86,7 @@ func (m *Message) generateAutoMessageID() string { dateMs := m.Date.UnixNano() / 1e6 h := fnv.New64() if len(m.To) > 0 { - _, _ = h.Write([]byte(m.To[0])) + _, _ = h.Write([]byte(m.To)) } _, _ = h.Write([]byte(m.Subject)) _, _ = h.Write([]byte(m.Body)) @@ -94,7 +94,7 @@ func (m *Message) generateAutoMessageID() string { } // NewMessageFrom creates new mail message object with custom From header. -func NewMessageFrom(to []string, fromDisplayName, fromAddress, subject, body string) *Message { +func NewMessageFrom(to, fromDisplayName, fromAddress, subject, body string) *Message { log.Trace("NewMessageFrom (body):\n%s", body) return &Message{ @@ -109,7 +109,7 @@ func NewMessageFrom(to []string, fromDisplayName, fromAddress, subject, body str } // NewMessage creates new mail message object with default From header. -func NewMessage(to []string, subject, body string) *Message { +func NewMessage(to, subject, body string) *Message { return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body) } @@ -166,7 +166,7 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { defer conn.Close() var tlsconfig *tls.Config - if opts.Protocol == "smtps" || opts.Protocol == "smtp+startls" { + if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" { tlsconfig = &tls.Config{ InsecureSkipVerify: opts.ForceTrustServerCert, ServerName: opts.SMTPAddr, @@ -208,7 +208,7 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error { } } - if opts.Protocol == "smtp+startls" { + if opts.Protocol == "smtp+starttls" { hasStartTLS, _ := client.Extension("STARTTLS") if hasStartTLS { if err = client.StartTLS(tlsconfig); err != nil { diff --git a/services/mailer/mailer_test.go b/services/mailer/mailer_test.go index b94fce8443..5504fdda7d 100644 --- a/services/mailer/mailer_test.go +++ b/services/mailer/mailer_test.go @@ -22,17 +22,17 @@ func TestGenerateMessageID(t *testing.T) { setting.Domain = "localhost" date := time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) - m := NewMessageFrom(nil, "display-name", "from-address", "subject", "body") + m := NewMessageFrom("", "display-name", "from-address", "subject", "body") m.Date = date gm := m.ToMessage() assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) - m = NewMessageFrom([]string{"a@b.com"}, "display-name", "from-address", "subject", "body") + m = NewMessageFrom("a@b.com", "display-name", "from-address", "subject", "body") m.Date = date gm = m.ToMessage() assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) - m = NewMessageFrom([]string{"a@b.com"}, "display-name", "from-address", "subject", "body") + m = NewMessageFrom("a@b.com", "display-name", "from-address", "subject", "body") m.SetHeader("Message-ID", "") gm = m.ToMessage() assert.Equal(t, "", gm.GetHeader("Message-ID")[0]) diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go index 21d8c672dd..6044e4ebd2 100644 --- a/services/migrations/gitbucket.go +++ b/services/migrations/gitbucket.go @@ -34,10 +34,14 @@ func (f *GitBucketDownloaderFactory) New(ctx context.Context, opts base.MigrateO return nil, err } - baseURL := u.Scheme + "://" + u.Host fields := strings.Split(u.Path, "/") - oldOwner := fields[1] - oldName := strings.TrimSuffix(fields[2], ".git") + if len(fields) < 2 { + return nil, fmt.Errorf("invalid path: %s", u.Path) + } + baseURL := u.Scheme + "://" + u.Host + strings.TrimSuffix(strings.Join(fields[:len(fields)-2], "/"), "/git") + + oldOwner := fields[len(fields)-2] + oldName := strings.TrimSuffix(fields[len(fields)-1], ".git") log.Trace("Create GitBucket downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, oldOwner, oldName) return NewGitBucketDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil @@ -72,6 +76,7 @@ func (g *GitBucketDownloader) ColorFormat(s fmt.State) { func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader { githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName) githubDownloader.SkipReactions = true + githubDownloader.SkipReviews = true return &GitBucketDownloader{ githubDownloader, } diff --git a/services/migrations/github.go b/services/migrations/github.go index 016d058865..fc343ca70c 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -76,6 +76,7 @@ type GithubDownloaderV3 struct { curClientIdx int maxPerPage int SkipReactions bool + SkipReviews bool } // NewGithubDownloaderV3 creates a github Downloader via github v3 API @@ -805,6 +806,9 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques // GetReviews returns pull requests review func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { allReviews := make([]*base.Review, 0, g.maxPerPage) + if g.SkipReviews { + return allReviews, nil + } opt := &github.ListOptions{ PerPage: g.maxPerPage, } diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go index 90c1fcaef5..7bbbbb6168 100644 --- a/services/migrations/github_test.go +++ b/services/migrations/github_test.go @@ -18,7 +18,11 @@ import ( func TestGitHubDownloadRepo(t *testing.T) { GithubLimitRateRemaining = 3 // Wait at 3 remaining since we could have 3 CI in // - downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", os.Getenv("GITHUB_READ_TOKEN"), "go-gitea", "test_repo") + token := os.Getenv("GITHUB_READ_TOKEN") + if token == "" { + t.Skip("Skipping GitHub migration test because GITHUB_READ_TOKEN is empty") + } + downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", token, "go-gitea", "test_repo") err := downloader.RefreshRate() assert.NoError(t, err) diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index dfb21b884b..271fdf7f3d 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -282,7 +282,7 @@ func migrateRepository(doer *user_model.User, downloader base.Downloader, upload lbBatchSize = len(labels) } - if err := uploader.CreateLabels(labels...); err != nil { + if err := uploader.CreateLabels(labels[:lbBatchSize]...); err != nil { return err } labels = labels[lbBatchSize:] diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 6002e6b8ed..ac9c9cc76d 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/proxy" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -216,6 +217,8 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo return nil, false } + envs := proxy.EnvWithProxy(remoteURL.URL) + stdoutBuilder := strings.Builder{} stderrBuilder := strings.Builder{} if err := git.NewCommand(ctx, gitArgs...). @@ -223,6 +226,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo Run(&git.RunOpts{ Timeout: timeout, Dir: repoPath, + Env: envs, Stdout: &stdoutBuilder, Stderr: &stderrBuilder, }); err != nil { diff --git a/services/packages/auth.go b/services/packages/auth.go index 50212fccfd..49ff6c0a3d 100644 --- a/services/packages/auth.go +++ b/services/packages/auth.go @@ -11,6 +11,7 @@ import ( "time" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "github.com/golang-jwt/jwt/v4" @@ -42,9 +43,15 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) { } func ParseAuthorizationToken(req *http.Request) (int64, error) { - parts := strings.SplitN(req.Header.Get("Authorization"), " ", 2) + h := req.Header.Get("Authorization") + if h == "" { + return 0, nil + } + + parts := strings.SplitN(h, " ", 2) if len(parts) != 2 { - return 0, fmt.Errorf("no token") + log.Error("split token failed: %s", h) + return 0, fmt.Errorf("split token failed") } token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (interface{}, error) { diff --git a/services/pull/check.go b/services/pull/check.go index 830ff640b5..1780f1e989 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -200,19 +200,19 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com return nil, fmt.Errorf("ReadFile(%s): %w", headFile, err) } commitID := string(commitIDBytes) - if len(commitID) < 40 { + if len(commitID) < git.SHAFullLength { return nil, fmt.Errorf(`ReadFile(%s): invalid commit-ID "%s"`, headFile, commitID) } - cmd := commitID[:40] + ".." + pr.BaseBranch + cmd := commitID[:git.SHAFullLength] + ".." + pr.BaseBranch // Get the commit from BaseBranch where the pull request got merged mergeCommit, _, err := git.NewCommand(ctx, "rev-list", "--ancestry-path", "--merges", "--reverse").AddDynamicArguments(cmd). RunStdString(&git.RunOpts{Dir: "", Env: []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()}}) if err != nil { return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %w", err) - } else if len(mergeCommit) < 40 { + } else if len(mergeCommit) < git.SHAFullLength { // PR was maybe fast-forwarded, so just use last commit of PR - mergeCommit = commitID[:40] + mergeCommit = commitID[:git.SHAFullLength] } gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) @@ -221,9 +221,9 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com } defer gitRepo.Close() - commit, err := gitRepo.GetCommit(mergeCommit[:40]) + commit, err := gitRepo.GetCommit(mergeCommit[:git.SHAFullLength]) if err != nil { - return nil, fmt.Errorf("GetMergeCommit[%v]: %w", mergeCommit[:40], err) + return nil, fmt.Errorf("GetMergeCommit[%v]: %w", mergeCommit[:git.SHAFullLength], err) } return commit, nil diff --git a/services/pull/merge.go b/services/pull/merge.go index 0ca3730183..3a67919f48 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -99,6 +99,9 @@ func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *issues_model.PullRe } for _, ref := range refs { if ref.RefAction == references.XRefActionCloses { + if err := ref.LoadIssue(); err != nil { + return "", err + } closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index)) } } @@ -513,7 +516,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode } sig := pr.Issue.Poster.NewGitSig() if signArg == "" { - if err := git.NewCommand(ctx, "commit", git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email)), "-m").AddDynamicArguments(message). + if err := git.NewCommand(ctx, "commit", git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email)), git.CmdArg("--message="+message)). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -531,7 +534,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode if err := git.NewCommand(ctx, "commit"). AddArguments(signArg). AddArguments(git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email))). - AddArguments("-m").AddDynamicArguments(message). + AddArguments(git.CmdArg("--message=" + message)). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -584,19 +587,25 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode headUser = pr.HeadRepo.Owner } - env = repo_module.FullPushingEnvironment( - headUser, - doer, - pr.BaseRepo, - pr.BaseRepo.Name, - pr.ID, - ) - var pushCmd *git.Command if mergeStyle == repo_model.MergeStyleRebaseUpdate { // force push the rebase result to head branch + env = repo_module.FullPushingEnvironment( + headUser, + doer, + pr.HeadRepo, + pr.HeadRepo.Name, + pr.ID, + ) pushCmd = git.NewCommand(ctx, "push", "-f", "head_repo").AddDynamicArguments(stagingBranch + ":" + git.BranchPrefix + pr.HeadBranch) } else { + env = repo_module.FullPushingEnvironment( + headUser, + doer, + pr.BaseRepo, + pr.BaseRepo.Name, + pr.ID, + ) pushCmd = git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) } @@ -635,7 +644,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, message string, signArg git.CmdArg, tmpBasePath string, env []string) error { var outbuf, errbuf strings.Builder if signArg == "" { - if err := git.NewCommand(ctx, "commit", "-m").AddDynamicArguments(message). + if err := git.NewCommand(ctx, "commit", git.CmdArg("--message="+message)). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -646,7 +655,7 @@ func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, me return fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } } else { - if err := git.NewCommand(ctx, "commit").AddArguments(signArg).AddArguments("-m").AddDynamicArguments(message). + if err := git.NewCommand(ctx, "commit").AddArguments(signArg).AddArguments(git.CmdArg("--message=" + message)). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -836,7 +845,7 @@ func MergedManually(pr *issues_model.PullRequest, doer *user_model.User, baseGit return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged} } - if len(commitID) < 40 { + if len(commitID) < git.SHAFullLength { return fmt.Errorf("Wrong commit ID") } diff --git a/services/pull/patch.go b/services/pull/patch.go index 9b87ac22e2..fdfbf7e072 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/gobwas/glob" @@ -53,6 +54,8 @@ var patchErrorSuffices = []string{ ": patch does not apply", ": already exists in working directory", "unrecognized input", + ": No such file or directory", + ": does not exist in index", } // TestPatch will test whether a simple patch will apply @@ -287,13 +290,15 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * // 2. AttemptThreeWayMerge first - this is much quicker than plain patch to base description := fmt.Sprintf("PR[%d] %s/%s#%d", pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index) - conflict, _, err := AttemptThreeWayMerge(ctx, + conflict, conflictFiles, err := AttemptThreeWayMerge(ctx, tmpBasePath, gitRepo, pr.MergeBase, "base", "tracking", description) if err != nil { return false, err } if !conflict { + // No conflicts detected so we need to check if the patch is empty... + // a. Write the newly merged tree and check the new tree-hash var treeHash string treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: tmpBasePath}) if err != nil { @@ -305,6 +310,8 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * if err != nil { return false, err } + + // b. compare the new tree-hash with the base tree hash if treeHash == baseTree.ID.String() { log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) pr.Status = issues_model.PullRequestStatusEmpty @@ -313,9 +320,17 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * return false, nil } - // 3. OK read-tree has failed so we need to try a different thing - this might actually succeed where the above fails due to whitespace handling. + // 3. OK the three-way merge method has detected conflicts + // 3a. Are still testing with GitApply? If not set the conflict status and move on + if !setting.Repository.PullRequest.TestConflictingPatchesWithGitApply { + pr.Status = issues_model.PullRequestStatusConflict + pr.ConflictedFiles = conflictFiles - // 3a. Create a plain patch from head to base + log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) + return true, nil + } + + // 3b. Create a plain patch from head to base tmpPatchFile, err := os.CreateTemp("", "patch") if err != nil { log.Error("Unable to create temporary patch file! Error: %v", err) @@ -338,8 +353,9 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * patchPath := tmpPatchFile.Name() tmpPatchFile.Close() - // 3b. if the size of that patch is 0 - there can be no conflicts! + // 3c. if the size of that patch is 0 - there can be no conflicts! if stat.Size() == 0 { + log.Critical("git-apply--check patch checker found empty PR when read-tree found conflicts in PR#%d[%d] in %#-v", pr.Index, pr.ID, pr.BaseRepo) log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID) pr.Status = issues_model.PullRequestStatusEmpty return false, nil @@ -416,6 +432,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * scanner := bufio.NewScanner(stderrReader) for scanner.Scan() { line := scanner.Text() + log.Trace("PullRequest[%d].testPatch: stderr: %s", pr.ID, line) if strings.HasPrefix(line, prefix) { conflict = true filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]) @@ -469,6 +486,8 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * } else if err != nil { return false, fmt.Errorf("git apply --check: %w", err) } + + log.Critical("git-apply--check patch checker found no conflicts when read-tree found conflicts in PR#%d[%d] in %#-v", pr.Index, pr.ID, pr.BaseRepo) return false, nil } diff --git a/services/pull/pull.go b/services/pull/pull.go index 5f8bd6b671..fbfc31d4e7 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -5,14 +5,12 @@ package pull import ( - "bufio" - "bytes" "context" "fmt" "io" + "os" "regexp" "strings" - "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" @@ -30,6 +28,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sync" + "code.gitea.io/gitea/modules/util" issue_service "code.gitea.io/gitea/services/issue" ) @@ -352,69 +351,56 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, // checkIfPRContentChanged checks if diff to target branch has changed by push // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) { - if err = pr.LoadHeadRepoCtx(ctx); err != nil { - return false, fmt.Errorf("LoadHeadRepo: %w", err) - } else if pr.HeadRepo == nil { - // corrupt data assumed changed - return true, nil + tmpBasePath, err := createTemporaryRepo(ctx, pr) + if err != nil { + log.Error("CreateTemporaryRepo: %v", err) + return false, err } + defer func() { + if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { + log.Error("checkIfPRContentChanged: RemoveTemporaryPath: %s", err) + } + }() - if err = pr.LoadBaseRepoCtx(ctx); err != nil { - return false, fmt.Errorf("LoadBaseRepo: %w", err) - } - - headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) + tmpRepo, err := git.OpenRepository(ctx, tmpBasePath) if err != nil { return false, fmt.Errorf("OpenRepository: %w", err) } - defer headGitRepo.Close() + defer tmpRepo.Close() - // Add a temporary remote. - tmpRemote := "checkIfPRContentChanged-" + fmt.Sprint(time.Now().UnixNano()) - if err = headGitRepo.AddRemote(tmpRemote, pr.BaseRepo.RepoPath(), true); err != nil { - return false, fmt.Errorf("AddRemote: %s/%s-%s: %w", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err) - } - defer func() { - if err := headGitRepo.RemoveRemote(tmpRemote); err != nil { - log.Error("checkIfPRContentChanged: RemoveRemote: %s/%s-%s: %v", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err) - } - }() - // To synchronize repo and get a base ref - _, base, err := headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch) + // Find the merge-base + _, base, err := tmpRepo.GetMergeBase("", "base", "tracking") if err != nil { return false, fmt.Errorf("GetMergeBase: %w", err) } - diffBefore := &bytes.Buffer{} - diffAfter := &bytes.Buffer{} - if err := headGitRepo.GetDiffFromMergeBase(base, oldCommitID, diffBefore); err != nil { - // If old commit not found, assume changed. - log.Debug("GetDiffFromMergeBase: %v", err) - return true, nil - } - if err := headGitRepo.GetDiffFromMergeBase(base, newCommitID, diffAfter); err != nil { - // New commit should be found - return false, fmt.Errorf("GetDiffFromMergeBase: %w", err) + cmd := git.NewCommand(ctx, "diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base) + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return false, fmt.Errorf("unable to open pipe for to run diff: %w", err) } - diffBeforeLines := bufio.NewScanner(diffBefore) - diffAfterLines := bufio.NewScanner(diffAfter) - - for diffBeforeLines.Scan() && diffAfterLines.Scan() { - if strings.HasPrefix(diffBeforeLines.Text(), "index") && strings.HasPrefix(diffAfterLines.Text(), "index") { - // file hashes can change without the diff changing - continue - } else if strings.HasPrefix(diffBeforeLines.Text(), "@@") && strings.HasPrefix(diffAfterLines.Text(), "@@") { - // the location of the difference may change - continue - } else if !bytes.Equal(diffBeforeLines.Bytes(), diffAfterLines.Bytes()) { + if err := cmd.Run(&git.RunOpts{ + Dir: tmpBasePath, + Stdout: stdoutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + defer func() { + _ = stdoutReader.Close() + }() + return util.IsEmptyReader(stdoutReader) + }, + }); err != nil { + if err == util.ErrNotEmpty { return true, nil } - } - if diffBeforeLines.Scan() || diffAfterLines.Scan() { - // Diffs not of equal length - return true, nil + log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v", + newCommitID, oldCommitID, base, + pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch, + err) + + return false, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, base, err) } return false, nil diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 15e776c4b9..4bc397cd4c 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -167,7 +167,7 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str var headBranch string if pr.Flow == issues_model.PullRequestFlowGithub { headBranch = git.BranchPrefix + pr.HeadBranch - } else if len(pr.HeadCommitID) == 40 { // for not created pull request + } else if len(pr.HeadCommitID) == git.SHAFullLength { // for not created pull request headBranch = pr.HeadCommitID } else { headBranch = pr.GetGitRefName() diff --git a/services/pull/update.go b/services/pull/update.go index bd4880a2fc..8aed60e070 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -109,6 +109,9 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, if pr.ProtectedBranch == nil { prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests) if err != nil { + if repo_model.IsErrUnitTypeNotExist(err) { + return false, false, nil + } log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err) return false, false, err } diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 3b986c66c6..7e5b103c5a 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "os" + "path" "path/filepath" "strings" @@ -218,21 +219,21 @@ func DeleteUnadoptedRepository(doer, u *user_model.User, repoName string) error return util.RemoveAll(repoPath) } -type unadoptedRrepositories struct { +type unadoptedRepositories struct { repositories []string index int start int end int } -func (unadopted *unadoptedRrepositories) add(repository string) { +func (unadopted *unadoptedRepositories) add(repository string) { if unadopted.index >= unadopted.start && unadopted.index < unadopted.end { unadopted.repositories = append(unadopted.repositories, repository) } unadopted.index++ } -func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unadopted *unadoptedRrepositories) error { +func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unadopted *unadoptedRepositories) error { if len(repoNamesToCheck) == 0 { return nil } @@ -264,7 +265,7 @@ func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unad } for _, repoName := range repoNamesToCheck { if !repoNames.Contains(repoName) { - unadopted.add(filepath.Join(userName, repoName)) + unadopted.add(path.Join(userName, repoName)) // These are not used as filepaths - but as reponames - therefore use path.Join not filepath.Join } } return nil @@ -292,7 +293,7 @@ func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, in var repoNamesToCheck []string start := (opts.Page - 1) * opts.PageSize - unadopted := &unadoptedRrepositories{ + unadopted := &unadoptedRepositories{ repositories: make([]string, 0, opts.PageSize), start: start, end: start + opts.PageSize, @@ -337,7 +338,7 @@ func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, in } repoNamesToCheck = append(repoNamesToCheck, name) - if len(repoNamesToCheck) > setting.Database.IterateBufferSize { + if len(repoNamesToCheck) >= setting.Database.IterateBufferSize { if err = checkUnadoptedRepositories(userName, repoNamesToCheck, unadopted); err != nil { return err } diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go index 685bfe9bc4..b450005f34 100644 --- a/services/repository/adopt_test.go +++ b/services/repository/adopt_test.go @@ -19,7 +19,7 @@ import ( func TestCheckUnadoptedRepositories_Add(t *testing.T) { start := 10 end := 20 - unadopted := &unadoptedRrepositories{ + unadopted := &unadoptedRepositories{ start: start, end: end, index: 0, @@ -39,7 +39,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) { // // Non existent user // - unadopted := &unadoptedRrepositories{start: 0, end: 100} + unadopted := &unadoptedRepositories{start: 0, end: 100} err := checkUnadoptedRepositories("notauser", []string{"repo"}, unadopted) assert.NoError(t, err) assert.Equal(t, 0, len(unadopted.repositories)) @@ -50,14 +50,14 @@ func TestCheckUnadoptedRepositories(t *testing.T) { userName := "user2" repoName := "repo2" unadoptedRepoName := "unadopted" - unadopted = &unadoptedRrepositories{start: 0, end: 100} + unadopted = &unadoptedRepositories{start: 0, end: 100} err = checkUnadoptedRepositories(userName, []string{repoName, unadoptedRepoName}, unadopted) assert.NoError(t, err) assert.Equal(t, []string{path.Join(userName, unadoptedRepoName)}, unadopted.repositories) // // Existing (adopted) repository is not returned // - unadopted = &unadoptedRrepositories{start: 0, end: 100} + unadopted = &unadoptedRepositories{start: 0, end: 100} err = checkUnadoptedRepositories(userName, []string{repoName}, unadopted) assert.NoError(t, err) assert.Equal(t, 0, len(unadopted.repositories)) diff --git a/services/repository/check.go b/services/repository/check.go index 5529a61b39..7e243cb3bb 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -73,32 +73,8 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...git.CmdArg) return db.ErrCancelledf("before GC of %s", repo.FullName()) default: } - log.Trace("Running git gc on %v", repo) - command := git.NewCommand(ctx, args...). - SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())) - var stdout string - var err error - stdout, _, err = command.RunStdString(&git.RunOpts{Timeout: timeout, Dir: repo.RepoPath()}) - - if err != nil { - log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) - desc := fmt.Sprintf("Repository garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) - if err = system_model.CreateRepositoryNotice(desc); err != nil { - log.Error("CreateRepositoryNotice: %v", err) - } - return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %w", repo.FullName(), err) - } - - // Now update the size of the repository - if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { - log.Error("Updating size as part of garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) - desc := fmt.Sprintf("Updating size as part of garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) - if err = system_model.CreateRepositoryNotice(desc); err != nil { - log.Error("CreateRepositoryNotice: %v", err) - } - return fmt.Errorf("Updating size as part of garbage collection failed in repo: %s: Error: %w", repo.FullName(), err) - } - + // we can ignore the error here because it will be logged in GitGCRepo + _ = GitGcRepo(ctx, repo, timeout, args) return nil }, ); err != nil { @@ -109,6 +85,37 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...git.CmdArg) return nil } +// GitGcRepo calls 'git gc' to remove unnecessary files and optimize the local repository +func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Duration, args []git.CmdArg) error { + log.Trace("Running git gc on %-v", repo) + command := git.NewCommand(ctx, args...). + SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName())) + var stdout string + var err error + stdout, _, err = command.RunStdString(&git.RunOpts{Timeout: timeout, Dir: repo.RepoPath()}) + + if err != nil { + log.Error("Repository garbage collection failed for %-v. Stdout: %s\nError: %v", repo, stdout, err) + desc := fmt.Sprintf("Repository garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) + if err := system_model.CreateRepositoryNotice(desc); err != nil { + log.Error("CreateRepositoryNotice: %v", err) + } + return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %w", repo.FullName(), err) + } + + // Now update the size of the repository + if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Updating size as part of garbage collection failed for %-v. Stdout: %s\nError: %v", repo, stdout, err) + desc := fmt.Sprintf("Updating size as part of garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) + if err := system_model.CreateRepositoryNotice(desc); err != nil { + log.Error("CreateRepositoryNotice: %v", err) + } + return fmt.Errorf("Updating size as part of garbage collection failed in repo: %s: Error: %w", repo.FullName(), err) + } + + return nil +} + func gatherMissingRepoRecords(ctx context.Context) ([]*repo_model.Repository, error) { repos := make([]*repo_model.Repository, 0, 10) if err := db.Iterate( @@ -162,7 +169,7 @@ func DeleteMissingRepositories(ctx context.Context, doer *user_model.User) error } log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { - log.Error("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err) + log.Error("Failed to DeleteRepository %-v: Error: %v", repo, err) if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err) } diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go index bc5a4c8ed3..72e2279ae5 100644 --- a/services/repository/files/commit.go +++ b/services/repository/files/commit.go @@ -30,9 +30,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato } defer closer.Close() - if _, err := gitRepo.GetCommit(sha); err != nil { + if commit, err := gitRepo.GetCommit(sha); err != nil { gitRepo.Close() return fmt.Errorf("GetCommit[%s]: %w", sha, err) + } else if len(sha) != git.SHAFullLength { + // use complete commit sha + sha = commit.ID.String() } gitRepo.Close() diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 34c8aeec25..91e002188e 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) // ContentType repo content type @@ -159,7 +160,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref return nil, fmt.Errorf("no commit found for the ref [ref: %s]", ref) } - selfURL, err := url.Parse(fmt.Sprintf("%s/contents/%s?ref=%s", repo.APIURL(), treePath, origRef)) + selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(treePath) + "?ref=" + url.QueryEscape(origRef)) if err != nil { return nil, err } @@ -218,7 +219,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref } // Handle links if entry.IsRegular() || entry.IsLink() { - downloadURL, err := url.Parse(fmt.Sprintf("%s/raw/%s/%s/%s", repo.HTMLURL(), refType, ref, treePath)) + downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath)) if err != nil { return nil, err } @@ -226,7 +227,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref contentsResponse.DownloadURL = &downloadURLString } if !entry.IsSubModule() { - htmlURL, err := url.Parse(fmt.Sprintf("%s/src/%s/%s/%s", repo.HTMLURL(), refType, ref, treePath)) + htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath)) if err != nil { return nil, err } @@ -234,7 +235,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref contentsResponse.HTMLURL = &htmlURLString contentsResponse.Links.HTMLURL = &htmlURLString - gitURL, err := url.Parse(fmt.Sprintf("%s/git/blobs/%s", repo.APIURL(), entry.ID.String())) + gitURL, err := url.Parse(repo.APIURL() + "/git/blobs/" + url.PathEscape(entry.ID.String())) if err != nil { return nil, err } diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 59e5690977..1df1cb582b 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -50,7 +50,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git copy(treeURL[apiURLLen:], "/git/trees/") // 40 is the size of the sha1 hash in hexadecimal format. - copyPos := len(treeURL) - 40 + copyPos := len(treeURL) - git.SHAFullLength if perPage <= 0 || perPage > setting.API.DefaultGitTreesPerPage { perPage = setting.API.DefaultGitTreesPerPage @@ -86,6 +86,11 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git if entries[e].IsDir() { copy(treeURL[copyPos:], entries[e].ID.String()) tree.Entries[i].URL = string(treeURL) + } else if entries[e].IsSubModule() { + // In Github Rest API Version=2022-11-28, if a tree entry is a submodule, + // its url will be returned as an empty string. + // So the URL will be set to "" here. + tree.Entries[i].URL = "" } else { copy(blobURL[copyPos:], entries[e].ID.String()) tree.Entries[i].URL = string(blobURL) diff --git a/services/repository/push.go b/services/repository/push.go index 3a7205d18b..dec5222432 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -104,6 +104,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { var pusher *user_model.User for _, opts := range optsList { + log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName) + if opts.IsNewRef() && opts.IsDelRef() { return fmt.Errorf("old and new revisions are both %s", git.EmptySHA) } @@ -129,7 +131,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } else { // is new tag newCommit, err := gitRepo.GetCommit(opts.NewCommitID) if err != nil { - return fmt.Errorf("gitRepo.GetCommit: %w", err) + return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) } commits := repo_module.NewPushCommits() @@ -162,7 +164,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { newCommit, err := gitRepo.GetCommit(opts.NewCommitID) if err != nil { - return fmt.Errorf("gitRepo.GetCommit: %w", err) + return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) } refName := opts.RefName() diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 74a69c297c..af45369d53 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -116,6 +116,10 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { event := t.EventType.Event() eventType := string(t.EventType) + req.Header.Add("X-Forgejo-Delivery", t.UUID) + req.Header.Add("X-Forgejo-Event", event) + req.Header.Add("X-Forgejo-Event-Type", eventType) + req.Header.Add("X-Forgejo-Signature", signatureSHA256) req.Header.Add("X-Gitea-Delivery", t.UUID) req.Header.Add("X-Gitea-Event", event) req.Header.Add("X-Gitea-Event-Type", eventType) diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 22d75db893..9511258eea 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -55,7 +55,7 @@ type ( Wait bool `json:"wait"` Content string `json:"content"` Username string `json:"username"` - AvatarURL string `json:"avatar_url"` + AvatarURL string `json:"avatar_url,omitempty"` TTS bool `json:"tts"` Embeds []DiscordEmbed `json:"embeds"` } diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 342e764f4d..5205cbcdc8 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -74,7 +74,7 @@ func RegisterWebhook(name string, webhook *webhook) { // IsValidHookTaskType returns true if a webhook registered func IsValidHookTaskType(name string) bool { - if name == webhook_model.GITEA || name == webhook_model.GOGS { + if name == webhook_model.FORGEJO || name == webhook_model.GITEA || name == webhook_model.GOGS { return true } _, ok := webhooks[name] @@ -170,7 +170,7 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook // Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.). // Integration webhooks (e.g. drone) still receive the required data. if pushEvent, ok := p.(*api.PushPayload); ok && - w.Type != webhook_model.GITEA && w.Type != webhook_model.GOGS && + w.Type != webhook_model.FORGEJO && w.Type != webhook_model.GITEA && w.Type != webhook_model.GOGS && len(pushEvent.Commits) == 0 { return nil } diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 5344ccaa22..acaa12253b 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -139,7 +139,7 @@ func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloade func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) { var text, title string switch p.Action { - case api.HookIssueSynchronized: + case api.HookIssueReviewed: action, err := parseHookPullRequestEventType(event) if err != nil { return nil, err diff --git a/templates/admin/applications/list.tmpl b/templates/admin/applications/list.tmpl index 6d627129df..4da6cb0446 100644 --- a/templates/admin/applications/list.tmpl +++ b/templates/admin/applications/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
diff --git a/templates/admin/applications/oauth2_edit.tmpl b/templates/admin/applications/oauth2_edit.tmpl index 84d821ecca..20231c4b1c 100644 --- a/templates/admin/applications/oauth2_edit.tmpl +++ b/templates/admin/applications/oauth2_edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}} {{template "user/settings/applications_oauth2_edit_form" .}} diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index bf9d53152c..91e4b1df52 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl index c43287ee1a..afe814cc6c 100644 --- a/templates/admin/auth/list.tmpl +++ b/templates/admin/auth/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 213c621b42..ab84dfccaf 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 982cfb2800..8f572c8396 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} @@ -303,14 +303,14 @@
{{.locale.Tr "admin.config.disable_gravatar"}}
- +
{{.locale.Tr "admin.config.enable_federated_avatar"}}
- +
diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 80eea91210..40f28068d7 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index adf5b9bef7..b26d26b524 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index c5196fce4e..51dd64df2f 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} @@ -14,8 +14,10 @@ {{.locale.Tr "admin.defaulthooks.update_webhook"}} {{end}}
- {{if eq .HookType "gitea"}} - + {{if eq .HookType "forgejo"}} + + {{else if eq .HookType "gitea"}} + {{else if eq .HookType "gogs"}} {{else if eq .HookType "slack"}} @@ -40,6 +42,7 @@
+ {{template "repo/settings/webhook/forgejo" .}} {{template "repo/settings/webhook/gitea" .}} {{template "repo/settings/webhook/gogs" .}} {{template "repo/settings/webhook/slack" .}} diff --git a/templates/admin/hooks.tmpl b/templates/admin/hooks.tmpl index a23cff4342..26f92c7064 100644 --- a/templates/admin/hooks.tmpl +++ b/templates/admin/hooks.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/monitor.tmpl b/templates/admin/monitor.tmpl index f11d071ea4..d53e9e18dc 100644 --- a/templates/admin/monitor.tmpl +++ b/templates/admin/monitor.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index 2777741efb..058f8c0d30 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl index 11dc23c60e..9bf7a6268e 100644 --- a/templates/admin/org/list.tmpl +++ b/templates/admin/org/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 3aab2873c6..37fa64d189 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/queue.tmpl b/templates/admin/queue.tmpl index cd50798f80..1905f7adb3 100644 --- a/templates/admin/queue.tmpl +++ b/templates/admin/queue.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 837802f0d0..11216b8c86 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index 0c27c80e93..ca0b4c3bb9 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index 91929deaa8..4e16036ae3 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 9e0f1d89fd..ef436c718a 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 56f6eaa3ad..88af2172b7 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} @@ -76,9 +76,9 @@ {{.locale.Tr "admin.users.2fa"}} {{.locale.Tr "admin.users.repos"}} {{.locale.Tr "admin.users.created"}} - + {{.locale.Tr "admin.users.last_login"}} - {{SortArrow "leastupdate" "recentupdate" $.SortType false}} + {{SortArrow "lastlogin" "reverselastlogin" $.SortType false}} {{.locale.Tr "admin.users.edit"}} diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 9fdf0dce93..e5ca864cb7 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
+
{{template "admin/navbar" .}}
{{template "base/alert" .}} diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl index 89be609225..9290b50ecb 100644 --- a/templates/base/footer_content.tmpl +++ b/templates/base/footer_content.tmpl @@ -1,7 +1,7 @@ -