diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 31e42d0ab30d..be34bfd02d31 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,9 @@ "ghcr.io/devcontainers/features/node:1": { "version":"20" }, - "ghcr.io/devcontainers/features/git-lfs:1.1.0": {} + "ghcr.io/devcontainers/features/git-lfs:1.1.0": {}, + "ghcr.io/devcontainers-contrib/features/poetry:2": {}, + "ghcr.io/devcontainers/features/python:1": {} }, "customizations": { "vscode": { diff --git a/.eslintrc.yaml b/.eslintrc.yaml index ea85ab12981f..71d8dc38148d 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -25,10 +25,11 @@ env: es2022: true node: true -globals: - __webpack_public_path__: true - overrides: + - files: ["web_src/**/*"] + globals: + __webpack_public_path__: true + process: false # https://github.com/webpack/webpack/issues/15833 - files: ["web_src/**/*", "docs/**/*"] env: browser: true diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 398fb6eae392..0c382567cca7 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -15,12 +15,13 @@ on: value: ${{ jobs.detect.outputs.templates }} docker: value: ${{ jobs.detect.outputs.docker }} + swagger: + value: ${{ jobs.detect.outputs.swagger }} jobs: detect: runs-on: ubuntu-latest timeout-minutes: 3 - # Map a step output to a job output outputs: backend: ${{ steps.changes.outputs.backend }} frontend: ${{ steps.changes.outputs.frontend }} @@ -28,6 +29,7 @@ jobs: actions: ${{ steps.changes.outputs.actions }} templates: ${{ steps.changes.outputs.templates }} docker: ${{ steps.changes.outputs.docker }} + swagger: ${{ steps.changes.outputs.swagger }} steps: - uses: actions/checkout@v3 - uses: dorny/paths-filter@v2 @@ -37,14 +39,18 @@ jobs: backend: - "**/*.go" - "templates/**/*.tmpl" + - "assets/emoji.json" - "go.mod" - "go.sum" + - "Makefile" frontend: - "**/*.js" - "web_src/**" + - "assets/emoji.json" - "package.json" - "package-lock.json" + - "Makefile" docs: - "**/*.md" @@ -56,7 +62,12 @@ jobs: templates: - "templates/**/*.tmpl" - "poetry.lock" + docker: - "Dockerfile" - "Dockerfile.rootless" - "docker/**" + - "Makefile" + + swagger: + - "templates/swagger/v1_json.tmpl" diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 5e094f02c193..c8bef283a934 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -39,6 +39,18 @@ jobs: - run: make deps-py - run: make lint-templates + lint-swagger: + if: needs.files-changed.outputs.swagger == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + - run: make deps-frontend + - run: make lint-swagger + lint-go-windows: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed @@ -96,6 +108,7 @@ jobs: - run: make deps-frontend - run: make lint-frontend - run: make checks-frontend + - run: make test-frontend - run: make frontend backend: @@ -110,7 +123,7 @@ jobs: check-latest: true # no frontend build here as backend should be able to build # even without any frontend files - - run: make deps-backend deps-tools + - run: make deps-backend - run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag - name: build-backend-arm64 run: make backend # test cross compile diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml index 8aeb70618251..a96589f1e8f6 100644 --- a/.stylelintrc.yaml +++ b/.stylelintrc.yaml @@ -50,7 +50,7 @@ rules: declaration-no-important: null declaration-property-max-values: null declaration-property-unit-allowed-list: null - declaration-property-unit-disallowed-list: null + declaration-property-unit-disallowed-list: {line-height: [em]} declaration-property-value-allowed-list: null declaration-property-value-disallowed-list: null declaration-property-value-no-unknown: true diff --git a/MAINTAINERS b/MAINTAINERS index c3ac7b945f07..3cdd8307e058 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -52,3 +52,4 @@ Xinyi Gong (@HesterG) wxiaoguang (@wxiaoguang) Gary Moon (@garymoon) Philip Peterson (@philip-peterson) +Denys Konovalov (@denyskon) diff --git a/Makefile b/Makefile index 08d439f422a7..0c4b42a8c543 100644 --- a/Makefile +++ b/Makefile @@ -84,7 +84,7 @@ GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) ifneq ($(GITHUB_REF_TYPE),branch) VERSION ?= $(subst v,,$(GITHUB_REF_NAME)) - GITEA_VERSION ?= $(GITHUB_REF_NAME) + GITEA_VERSION ?= $(VERSION) else ifneq ($(GITHUB_REF_NAME),) VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME)) @@ -226,6 +226,8 @@ help: @echo " - test-frontend test frontend files" @echo " - test-backend test backend files" @echo " - test-e2e[\#TestSpecificName] test end to end using playwright" + @echo " - update-js update js dependencies" + @echo " - update-py update py dependencies" @echo " - webpack build webpack files" @echo " - svg build svg files" @echo " - fomantic build fomantic files" @@ -358,10 +360,10 @@ lint: lint-frontend lint-backend lint-fix: lint-frontend-fix lint-backend-fix .PHONY: lint-frontend -lint-frontend: lint-js lint-css lint-md lint-swagger +lint-frontend: lint-js lint-css .PHONY: lint-frontend-fix -lint-frontend-fix: lint-js-fix lint-css-fix lint-md lint-swagger +lint-frontend-fix: lint-js-fix lint-css-fix .PHONY: lint-backend lint-backend: lint-go lint-go-vet lint-editorconfig @@ -924,13 +926,20 @@ node_modules: package-lock.json poetry install @touch .venv -.PHONY: npm-update -npm-update: node-check | node_modules - npx updates -cu +.PHONY: update-js +update-js: node-check | node_modules + npx updates -u -f package.json rm -rf node_modules package-lock.json npm install --package-lock @touch node_modules +.PHONY: update-py +update-py: node-check | node_modules + npx updates -u -f pyproject.toml + rm -rf .venv poetry.lock + poetry install + @touch .venv + .PHONY: fomantic fomantic: rm -rf $(FOMANTIC_WORK_DIR)/build diff --git a/assets/emoji.json b/assets/emoji.json index 5a1ff98a46af..28244caa651c 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":["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 +[{"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_bird"]},{"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":["donkey"]},{"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":["flute"]},{"emoji":"๐Ÿชฐ","aliases":["fly"]},{"emoji":"๐Ÿฅ","aliases":["flying_disc"]},{"emoji":"๐Ÿ›ธ","aliases":["flying_saucer"]},{"emoji":"๐ŸŒซ๏ธ","aliases":["fog"]},{"emoji":"๐ŸŒ","aliases":["foggy"]},{"emoji":"๐Ÿชญ","aliases":["folding_hand_fan"]},{"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":["ginger_root"]},{"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":["goose"]},{"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_heart"]},{"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":["hair_pick"]},{"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":["hyacinth"]},{"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":["jellyfish"]},{"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":["khanda"]},{"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":["leftwards_pushing_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_blue_heart"]},{"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":["maracas"]},{"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":["moose"]},{"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":["pea_pod"]},{"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":["pink_heart"]},{"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":["rightwards_pushing_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":["shaking_face"]},{"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":["wing"]},{"emoji":"๐Ÿ˜‰","aliases":["wink"]},{"emoji":"๐Ÿ›œ","aliases":["wireless"]},{"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/build/generate-emoji.go b/build/generate-emoji.go index 09bdeb680828..17a9670f06a3 100644 --- a/build/generate-emoji.go +++ b/build/generate-emoji.go @@ -25,7 +25,7 @@ import ( const ( gemojiURL = "https://github.com/raw/github/gemoji/master/db/emoji.json" - maxUnicodeVersion = 14 + maxUnicodeVersion = 15 ) var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out") diff --git a/cmd/cmd.go b/cmd/cmd.go index 8076acecaa25..4ed636a9b4cc 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -106,5 +106,21 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) { WriterOption: log.WriterConsoleOption{Stderr: out == os.Stderr}, } writer := log.NewEventWriterConsole("console-default", writeMode) - log.GetManager().GetLogger(log.DEFAULT).RemoveAllWriters().AddWriters(writer) + log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) +} + +// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout. +// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever. +func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error { + return func(c *cli.Context) error { + level := defaultLevel + if c.Bool("quiet") || c.GlobalBoolT("quiet") { + level = log.FATAL + } + if c.Bool("debug") || c.GlobalBool("debug") || c.Bool("verbose") || c.GlobalBool("verbose") { + level = log.TRACE + } + log.SetConsoleLogger(log.DEFAULT, "console-default", level) + return nil + } } diff --git a/cmd/doctor.go b/cmd/doctor.go index b79436fc0a9f..cd5f125e20e4 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -151,7 +151,7 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { log.FallbackErrorf("unable to create file log writer: %v", err) return } - log.GetManager().GetLogger(log.DEFAULT).RemoveAllWriters().AddWriters(writer) + log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) } } diff --git a/cmd/embedded.go b/cmd/embedded.go index 204a623cf704..105acee26ce2 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -22,9 +22,9 @@ import ( "github.com/urfave/cli" ) -// Cmdembedded represents the available extract sub-command. +// CmdEmbedded represents the available extract sub-command. var ( - Cmdembedded = cli.Command{ + CmdEmbedded = cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", diff --git a/cmd/hook.go b/cmd/hook.go index 645326783283..ed6efc19ea2e 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -15,6 +15,7 @@ import ( "time" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -32,6 +33,7 @@ var ( Name: "hook", Usage: "Delegate commands to corresponding Git hooks", Description: "This should only be called by Git", + Before: PrepareConsoleLoggerLevel(log.FATAL), Subcommands: []cli.Command{ subcmdHookPreReceive, subcmdHookUpdate, diff --git a/cmd/keys.go b/cmd/keys.go index deb94fca5d6a..8aeee37527da 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" "github.com/urfave/cli" @@ -17,6 +18,7 @@ import ( var CmdKeys = cli.Command{ Name: "keys", Usage: "This command queries the Gitea database to get the authorized command for a given ssh key fingerprint", + Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runKeys, Flags: []cli.Flag{ cli.StringFlag{ diff --git a/cmd/serv.go b/cmd/serv.go index 01102d3800c0..79052e58a8f5 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -44,6 +44,7 @@ var CmdServ = cli.Command{ Name: "serv", Usage: "This command should only be called by SSH shell", Description: "Serv provides access auth for repositories", + Before: PrepareConsoleLoggerLevel(log.FATAL), Action: runServ, Flags: []cli.Flag{ cli.BoolFlag{ diff --git a/cmd/web.go b/cmd/web.go index 7a257a62a277..05f3b2ddb2ea 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -35,6 +35,7 @@ var CmdWeb = cli.Command{ Usage: "Start Gitea web server", Description: `Gitea web server is the only thing you need to run, and it takes care of all the other things for you`, + Before: PrepareConsoleLoggerLevel(log.INFO), Action: runWeb, Flags: []cli.Flag{ cli.StringFlag{ @@ -206,11 +207,6 @@ func servePprof() { } func runWeb(ctx *cli.Context) error { - if ctx.Bool("verbose") { - setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout) - } else if ctx.Bool("quiet") { - setupConsoleLogger(log.FATAL, log.CanColorStdout, os.Stdout) - } defer func() { if panicked := recover(); panicked != nil { log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2)) diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index 2cdf4e3943b4..230ed58269aa 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -88,7 +88,7 @@ func main() { } func runEnvironmentToIni(c *cli.Context) error { - setting.InitWorkPathAndCommonConfig(os.Getenv, setting.ArgWorkPathAndCustomConf{ + setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{ WorkPath: c.String("work-path"), CustomPath: c.String("custom-path"), CustomConf: c.String("config"), diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index e51f0558831a..83c713cb05e6 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -119,10 +119,13 @@ RUN_USER = ; git ;; Permission for unix socket ;UNIX_SOCKET_PERMISSION = 666 ;; -;; Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. -;; In most cases you do not need to change the default value. -;; Alter it only if your SSH server node is not the same as HTTP node. -;; Do not set this variable if PROTOCOL is set to 'unix'. +;; Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. In +;; most cases you do not need to change the default value. Alter it only if +;; your SSH server node is not the same as HTTP node. For different protocol, the default +;; values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. +;; If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. +;; If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default +;; value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. ;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/ ;; ;; When making local connections pass the PROXY protocol header. @@ -190,8 +193,8 @@ RUN_USER = ; git ;; Use `ssh-keygen` to parse public SSH keys. The value is passed to the shell. By default, Gitea does the parsing itself. ;SSH_KEYGEN_PATH = ;; -;; Enable SSH Authorized Key Backup when rewriting all keys, default is true -;SSH_AUTHORIZED_KEYS_BACKUP = true +;; Enable SSH Authorized Key Backup when rewriting all keys, default is false +;SSH_AUTHORIZED_KEYS_BACKUP = false ;; ;; Determines which principals to allow ;; - empty: if SSH_TRUSTED_USER_CA_KEYS is empty this will default to off, otherwise will default to email, username. @@ -300,7 +303,10 @@ RUN_USER = ; git ;; ;; ;; LFS authentication secret, change this yourself -LFS_JWT_SECRET = +;LFS_JWT_SECRET = +;; +;; Alternative location to specify LFS authentication secret. You cannot specify both this and LFS_JWT_SECRET, and must pick one +;LFS_JWT_SECRET_URI = file:/etc/gitea/lfs_jwt_secret ;; ;; LFS authentication validity period (in time.Duration), pushes taking longer than this may fail. ;LFS_HTTP_AUTH_EXPIRY = 24h @@ -344,9 +350,6 @@ NAME = gitea USER = root ;PASSWD = ;Use PASSWD = `your password` for quoting if you use special characters in the password. ;SSL_MODE = false ; either "false" (default), "true", or "skip-verify" -;CHARSET = utf8mb4 ;either "utf8" or "utf8mb4", default is "utf8mb4". -;; -;; NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; @@ -527,6 +530,9 @@ ENABLE = true ;; This setting is only needed if JWT_SIGNING_ALGORITHM is set to HS256, HS384 or HS512. ;JWT_SECRET = ;; +;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one +;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret +;; ;; Lifetime of an OAuth2 access token in seconds ;ACCESS_TOKEN_EXPIRATION_TIME = 3600 ;; @@ -1337,10 +1343,10 @@ LEVEL = Info ;; Issue indexer storage path, available when ISSUE_INDEXER_TYPE is bleve ;ISSUE_INDEXER_PATH = indexers/issues.bleve ; Relative paths will be made absolute against _`AppWorkPath`_. ;; -;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch -;ISSUE_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 +;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch (e.g. http://elastic:password@localhost:9200) or meilisearch (e.g. http://:apikey@localhost:7700) +;ISSUE_INDEXER_CONN_STR = ;; -;; Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch +;; Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch. ;ISSUE_INDEXER_NAME = gitea_issues ;; ;; Timeout the indexer if it takes longer than this to start. @@ -2541,8 +2547,8 @@ LEVEL = Info ;; Enable/Disable actions capabilities ;ENABLED = false ;; -;; Default address to get action plugins, e.g. the default value means downloading from "https://gitea.com/actions/checkout" for "uses: actions/checkout@v3" -;DEFAULT_ACTIONS_URL = https://gitea.com +;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. +;DEFAULT_ACTIONS_URL = github ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docker/manifest.rootless.tmpl b/docker/manifest.rootless.tmpl index 87e6e9dfc2b5..1ebf5b73c847 100644 --- a/docker/manifest.rootless.tmpl +++ b/docker/manifest.rootless.tmpl @@ -1,12 +1,14 @@ image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-rootless {{#if build.tags}} {{#unless (contains "-rc" build.tag)}} +{{#unless (contains "-dev" build.tag)}} tags: {{#each build.tags}} - {{this}}-rootless {{/each}} - "latest-rootless" {{/unless}} +{{/unless}} {{/if}} manifests: - diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl index 18847064f624..08ccf61b5785 100644 --- a/docker/manifest.tmpl +++ b/docker/manifest.tmpl @@ -1,12 +1,14 @@ image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}} {{#if build.tags}} {{#unless (contains "-rc" build.tag)}} +{{#unless (contains "-dev" build.tag)}} tags: {{#each build.tags}} - {{this}} {{/each}} - "latest" {{/unless}} +{{/unless}} {{/if}} manifests: - diff --git a/docs/content/doc/administration/command-line.en-us.md b/docs/content/doc/administration/command-line.en-us.md index 37ba0c04da61..d7e74ee24df9 100644 --- a/docs/content/doc/administration/command-line.en-us.md +++ b/docs/content/doc/administration/command-line.en-us.md @@ -108,6 +108,14 @@ Admin operations: - `--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 + - `generate-access-token`: + - Options: + - `--username value`, `-u value`: Username. Required. + - `--token-name value`, `-t value`: Token name. Required. + - `--scopes value`: Comma-separated list of scopes. Scopes follow the format `[read|write]:` or `all` where `` is one of the available visual groups you can see when opening the API page showing the available routes (for example `repo`). + - Examples: + - `gitea admin user generate-access-token --username myname --token-name mytoken` + - `gitea admin user generate-access-token --help` - `regenerate` - Options: - `hooks`: Regenerate Git Hooks for all repositories diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 64a356555478..9c307cbc48c7 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -314,8 +314,11 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `LOCAL_ROOT_URL`: **%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/**: Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service. In most cases you do not need to change the default value. Alter it only if - your SSH server node is not the same as HTTP node. Do not set this variable - if `PROTOCOL` is set to `http+unix`. + your SSH server node is not the same as HTTP node. For different protocol, the default + values are different. If `PROTOCOL` is `http+unix`, the default value is `http://unix/`. + If `PROTOCOL` is `fcgi` or `fcgi+unix`, the default value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. + If listen on `0.0.0.0`, the default value is `%(PROTOCOL)s://localhost:%(HTTP_PORT)s/`, Otherwise the default + value is `%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/`. - `LOCAL_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)s**: When making local connections pass the PROXY protocol header. This should be set to false if the local connection will go through the proxy. - `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to -1 to @@ -333,7 +336,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SSH_LISTEN_PORT`: **%(SSH\_PORT)s**: Port for the built-in SSH server. - `SSH_ROOT_PATH`: **~/.ssh**: Root path of SSH directory. - `SSH_CREATE_AUTHORIZED_KEYS_FILE`: **true**: Gitea will create a authorized_keys file by default when it is not using the internal ssh server. If you intend to use the AuthorizedKeysCommand functionality then you should turn this off. -- `SSH_AUTHORIZED_KEYS_BACKUP`: **true**: Enable SSH Authorized Key Backup when rewriting all keys, default is true. +- `SSH_AUTHORIZED_KEYS_BACKUP`: **false**: Enable SSH Authorized Key Backup when rewriting all keys, default is false. - `SSH_TRUSTED_USER_CA_KEYS`: **\**: Specifies the public keys of certificate authorities that are trusted to sign user certificates for authentication. Multiple keys should be comma separated. E.g.`ssh- ` or `ssh- , ssh- `. For more information see `TrustedUserCAKeys` in the sshd config man pages. When empty no file will be created and `SSH_AUTHORIZED_PRINCIPALS_ALLOW` will default to `off`. - `SSH_TRUSTED_USER_CA_KEYS_FILENAME`: **`RUN_USER`/.ssh/gitea-trusted-user-ca-keys.pem**: Absolute path of the `TrustedUserCaKeys` file Gitea will manage. If you're running your own ssh server and you want to use the Gitea managed file you'll also need to modify your sshd_config to point to this file. The official docker image will automatically work without further configuration. - `SSH_AUTHORIZED_PRINCIPALS_ALLOW`: **off** or **username, email**: \[off, username, email, anything\]: Specify the principals values that users are allowed to use as principal. When set to `anything` no checks are done on the principal string. When set to `off` authorized principal are not allowed to be set. @@ -365,6 +368,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `LFS_START_SERVER`: **false**: Enables Git LFS support. - `LFS_CONTENT_PATH`: **%(APP_DATA_PATH)s/lfs**: Default LFS content path. (if it is on local storage.) **DEPRECATED** use settings in `[lfs]`. - `LFS_JWT_SECRET`: **\**: LFS authentication secret, change this a unique string. +- `LFS_JWT_SECRET_URI`: **\**: Instead of defining LFS_JWT_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the secret (example value: `file:/etc/gitea/lfs_jwt_secret`) - `LFS_HTTP_AUTH_EXPIRY`: **24h**: LFS authentication validity period in time.Duration, pushes taking longer than this may fail. - `LFS_MAX_FILE_SIZE`: **0**: Maximum allowed LFS file size in bytes (Set to 0 for no limit). - `LFS_LOCKS_PAGING_NUM`: **50**: Maximum number of LFS Locks returned per page. @@ -443,7 +447,6 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SQLITE_TIMEOUT`: **500**: Query timeout for SQLite3 only. - `SQLITE_JOURNAL_MODE`: **""**: Change journal mode for SQlite3. Can be used to enable [WAL mode](https://www.sqlite.org/wal.html) when high load causes write congestion. See [SQlite3 docs](https://www.sqlite.org/pragma.html#pragma_journal_mode) for possible values. Defaults to the default for the database file, often DELETE. - `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating. -- `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. - `LOG_SQL`: **true**: Log the executed SQL. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. @@ -459,15 +462,15 @@ relation to port exhaustion. ## Indexer (`indexer`) - `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently supported: `bleve`, `db`, `elasticsearch` or `meilisearch`. -- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch, or meilisearch. i.e. http://elastic:changeme@localhost:9200 -- `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch +- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch (e.g. http://elastic:password@localhost:9200) or meilisearch (e.g. http://:apikey@localhost:7700) +- `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch or meilisearch. - `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch. Relative paths will be made absolute against _`AppWorkPath`_. - `REPO_INDEXER_ENABLED`: **false**: Enables code search (uses a lot of disk space, about 6 times more than the repository size). - `REPO_INDEXER_REPO_TYPES`: **sources,forks,mirrors,templates**: Repo indexer units. The items to index could be `sources`, `forks`, `mirrors`, `templates` or any combination of them separated by a comma. If empty then it defaults to `sources` only, as if you'd like to disable fully please see `REPO_INDEXER_ENABLED`. - `REPO_INDEXER_TYPE`: **bleve**: Code search engine type, could be `bleve` or `elasticsearch`. - `REPO_INDEXER_PATH`: **indexers/repos.bleve**: Index file used for code search. -- `REPO_INDEXER_CONN_STR`: ****: Code indexer connection string, available when `REPO_INDEXER_TYPE` is elasticsearch. i.e. http://elastic:changeme@localhost:9200 +- `REPO_INDEXER_CONN_STR`: ****: Code indexer connection string, available when `REPO_INDEXER_TYPE` is elasticsearch. i.e. http://elastic:password@localhost:9200 - `REPO_INDEXER_NAME`: **gitea_codes**: Code indexer name, available when `REPO_INDEXER_TYPE` is elasticsearch - `REPO_INDEXER_INCLUDE`: **empty**: A comma separated list of glob patterns (see https://github.com/gobwas/glob) to **include** in the index. Use `**.txt` to match any files with .txt extension. An empty list means include all files. @@ -1095,6 +1098,7 @@ This section only does "set" config, a removed config key from this section won' - `INVALIDATE_REFRESH_TOKENS`: **false**: Check if refresh token has already been used - `JWT_SIGNING_ALGORITHM`: **RS256**: Algorithm used to sign OAuth2 tokens. Valid values: \[`HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`\] - `JWT_SECRET`: **\**: OAuth2 authentication secret for access and refresh tokens, change this to a unique string. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `HS256`, `HS384` or `HS512`. +- `JWT_SECRET_URI`: **\**: Instead of defining JWT_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the secret (example value: `file:/etc/gitea/oauth2_jwt_secret`) - `JWT_SIGNING_PRIVATE_KEY_FILE`: **jwt/private.pem**: Private key file path used to sign OAuth2 tokens. The path is relative to `APP_DATA_PATH`. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `RS256`, `RS384`, `RS512`, `ES256`, `ES384` or `ES512`. The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you. - `MAX_TOKEN_LENGTH`: **32767**: Maximum length of token/cookie to accept from OAuth2 provider @@ -1374,39 +1378,22 @@ PROXY_HOSTS = *.github.com ## Actions (`actions`) - `ENABLED`: **false**: Enable/Disable actions capabilities -- `DEFAULT_ACTIONS_URL`: **https://gitea.com**: Default address to get action plugins, e.g. the default value means downloading from "" for "uses: actions/checkout@v3" +- `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. - `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` - `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` -`DEFAULT_ACTIONS_URL` indicates where should we find the relative path action plugin. i.e. when use an action in a workflow file like - -```yaml -name: versions -on: - push: - branches: - - main - - releases/* -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 -``` - -Now we need to know how to get actions/checkout, this configuration is the default git server to get it. That means we will get the repository via git clone ${DEFAULT_ACTIONS_URL}/actions/checkout and fetch tag v3. - -To help people who don't want to mirror these actions in their git instances, the default value is https://gitea.com -To help people run actions totally in their network, they can change the value and copy all necessary action repositories into their git server. +`DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. +For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`. +And it can be changed to `self` to make it `root_url_of_your_gitea/actions/checkout@v3`. -Of course we should support the form in future PRs like - -```yaml -steps: - - uses: gitea.com/actions/checkout@v3 -``` +Please note that using `self` is not recommended for most cases, as it could make names globally ambiguous. +Additionally, it requires you to mirror all the actions you need to your Gitea instance, which may not be worth it. +Therefore, please use `self` only if you understand what you are doing. -although Github don't support this form. +In earlier versions (<= 1.19), `DEFAULT_ACTIONS_URL` cound be set to any custom URLs like `https://gitea.com` or `http://your-git-server,https://gitea.com`, and the default value was `https://gitea.com`. +However, later updates removed those options, and now the only options are `github` and `self`, with the default value being `github`. +However, if you want to use actions from other git server, you can use a complete URL in `uses` field, it's supported by Gitea (but not GitHub). +Like `uses: https://gitea.com/actions/checkout@v3` or `uses: http://your-git-server/actions/checkout@v3`. ## Other (`other`) diff --git a/docs/content/doc/help/faq.en-us.md b/docs/content/doc/help/faq.en-us.md index f609b6c86704..ae59a9b8807c 100644 --- a/docs/content/doc/help/faq.en-us.md +++ b/docs/content/doc/help/faq.en-us.md @@ -396,8 +396,6 @@ Please run `gitea convert`, or run `ALTER DATABASE database_name CHARACTER SET u for the database_name and run `ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` for each table in the database. -You will also need to change the app.ini database charset to `CHARSET=utf8mb4`. - ## Why are Emoji displaying only as placeholders or in monochrome Gitea requires the system or browser to have one of the supported Emoji fonts installed, which are Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji and Twemoji Mozilla. Generally, the operating system should already provide one of these fonts, but especially on Linux, it may be necessary to install them manually. diff --git a/docs/content/doc/usage/actions/faq.en-us.md b/docs/content/doc/usage/actions/faq.en-us.md index 194c297a024b..69a4cf3e89ac 100644 --- a/docs/content/doc/usage/actions/faq.en-us.md +++ b/docs/content/doc/usage/actions/faq.en-us.md @@ -164,3 +164,23 @@ Although we would like to provide more options, our limited manpower means that However, both Gitea and act runner are completely open source, so anyone can create a new/better implementation. We support your choice, no matter how you decide. In case you fork act runner to create your own version: Please contribute the changes back if you can and if you think your changes will help others as well. + +## What workflow trigger events does Gitea support? + +All events listed in this table are supported events and are compatible with GitHub. +For events supported only by GitHub, see GitHub's [documentation](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows). + +| trigger event | activity types | +|-----------------------------|--------------------------------------------------------------------------------------------------------------------------| +| create | not applicable | +| delete | not applicable | +| fork | not applicable | +| gollum | not applicable | +| push | not applicable | +| issues | `opened`, `edited`, `closed`, `reopened`, `assigned`, `unassigned`, `milestoned`, `demilestoned`, `labeled`, `unlabeled` | +| issue_comment | `created`, `edited`, `deleted` | +| pull_request | `opened`, `edited`, `closed`, `reopened`, `assigned`, `unassigned`, `synchronize`, `labeled`, `unlabeled` | +| pull_request_review | `submitted`, `edited` | +| pull_request_review_comment | `created`, `edited` | +| release | `published`, `edited` | +| registry_package | `published` | diff --git a/docs/content/doc/usage/actions/faq.zh-cn.md b/docs/content/doc/usage/actions/faq.zh-cn.md index c990c04f150a..ae6edd06f28c 100644 --- a/docs/content/doc/usage/actions/faq.zh-cn.md +++ b/docs/content/doc/usage/actions/faq.zh-cn.md @@ -164,3 +164,23 @@ defaults: ็„ถ่€Œ๏ผŒๆ— ่ฎบๆ‚จๅฆ‚ไฝ•ๅ†ณๅฎš๏ผŒGitea ๅ’Œact runner้ƒฝๆ˜ฏๅฎŒๅ…จๅผ€ๆบ็š„๏ผŒๆ‰€ไปฅไปปไฝ•ไบบ้ƒฝๅฏไปฅๅˆ›ๅปบไธ€ไธชๆ–ฐ็š„/ๆ›ดๅฅฝ็š„ๅฎž็Žฐใ€‚ ๆˆ‘ไปฌๆ”ฏๆŒๆ‚จ็š„้€‰ๆ‹ฉ๏ผŒๆ— ่ฎบๆ‚จๅฆ‚ไฝ•ๅ†ณๅฎšใ€‚ ๅฆ‚ๆžœๆ‚จ้€‰ๆ‹ฉๅˆ†ๆ”ฏact runnerๆฅๅˆ›ๅปบ่‡ชๅทฑ็š„็‰ˆๆœฌ๏ผŒ่ฏทๅœจๆ‚จ่ฎคไธบๆ‚จ็š„ๆ›ดๆ”นๅฏนๅ…ถไป–ไบบไนŸๆœ‰ๅธฎๅŠฉ็š„ๆƒ…ๅ†ตไธ‹่ดก็Œฎ่ฟ™ไบ›ๆ›ดๆ”นใ€‚ + +## Gitea ๆ”ฏๆŒๅ“ชไบ›ๅทฅไฝœๆต่งฆๅ‘ไบ‹ไปถ๏ผŸ + +่กจๆ ผไธญๅˆ—ๅ‡บ็š„ๆ‰€ๆœ‰ไบ‹ไปถ้ƒฝๆ˜ฏๆ”ฏๆŒ็š„๏ผŒๅนถไธ”ไธŽ GitHub ๅ…ผๅฎนใ€‚ +ๅฏนไบŽไป… GitHub ๆ”ฏๆŒ็š„ไบ‹ไปถ๏ผŒ่ฏทๅ‚้˜… GitHub ็š„[ๆ–‡ๆกฃ](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows)ใ€‚ + +| ่งฆๅ‘ไบ‹ไปถ | ๆดปๅŠจ็ฑปๅž‹ | +|-----------------------------|--------------------------------------------------------------------------------------------------------------------------| +| create | ไธ้€‚็”จ | +| delete | ไธ้€‚็”จ | +| fork | ไธ้€‚็”จ | +| gollum | ไธ้€‚็”จ | +| push | ไธ้€‚็”จ | +| issues | `opened`, `edited`, `closed`, `reopened`, `assigned`, `unassigned`, `milestoned`, `demilestoned`, `labeled`, `unlabeled` | +| issue_comment | `created`, `edited`, `deleted` | +| pull_request | `opened`, `edited`, `closed`, `reopened`, `assigned`, `unassigned`, `synchronize`, `labeled`, `unlabeled` | +| pull_request_review | `submitted`, `edited` | +| pull_request_review_comment | `created`, `edited` | +| release | `published`, `edited` | +| registry_package | `published` | diff --git a/go.mod b/go.mod index 7b7a51efbb7f..885bb3422046 100644 --- a/go.mod +++ b/go.mod @@ -122,7 +122,7 @@ require ( mvdan.cc/xurls/v2 v2.4.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.12 - xorm.io/xorm v1.3.3-0.20230219231735-056cecc97e9e + xorm.io/xorm v1.3.3-0.20230623150031-18f8e7a86c75 ) require ( diff --git a/go.sum b/go.sum index c3ac719f3f4a..9b4538bc65bc 100644 --- a/go.sum +++ b/go.sum @@ -1923,5 +1923,5 @@ strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1: xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM= xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/xorm v1.3.3-0.20230219231735-056cecc97e9e h1:d5PY6mwuQK5/7T6VKfFswaKMzLmGTHkJ/ZS7+cUIAjk= -xorm.io/xorm v1.3.3-0.20230219231735-056cecc97e9e/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= +xorm.io/xorm v1.3.3-0.20230623150031-18f8e7a86c75 h1:ReBAlO50dCIXCWF8Gbi0ZRa62AGAwCJNCPaUNUa7JSg= +xorm.io/xorm v1.3.3-0.20230623150031-18f8e7a86c75/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/main.go b/main.go index 1c87824c83ce..9b561376c34a 100644 --- a/main.go +++ b/main.go @@ -47,7 +47,9 @@ func init() { // ./gitea -h // ./gitea web help // ./gitea web -h (due to cli lib limitation, this won't call our cmdHelp, so no extra info) -// ./gitea admin help auth +// ./gitea admin +// ./gitea admin help +// ./gitea admin auth help // ./gitea -c /tmp/app.ini -h // ./gitea -c /tmp/app.ini help // ./gitea help -c /tmp/app.ini @@ -85,28 +87,36 @@ func main() { app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".` app.Version = Version + formatBuiltWith() app.EnableBashCompletion = true - app.Commands = []cli.Command{ + + // these sub-commands need to use config file + subCmdWithIni := []cli.Command{ cmd.CmdWeb, cmd.CmdServ, cmd.CmdHook, cmd.CmdDump, - cmd.CmdCert, cmd.CmdAdmin, - cmd.CmdGenerate, cmd.CmdMigrate, cmd.CmdKeys, cmd.CmdConvert, cmd.CmdDoctor, cmd.CmdManager, - cmd.Cmdembedded, + cmd.CmdEmbedded, cmd.CmdMigrateStorage, - cmd.CmdDocs, cmd.CmdDumpRepository, cmd.CmdRestoreRepository, cmd.CmdActions, + cmdHelp, // TODO: the "help" sub-command was used to show the more information for "work path" and "custom config", in the future, it should avoid doing so + } + // these sub-commands do not need the config file, and they do not depend on any path or environment variable. + subCmdStandalone := []cli.Command{ + cmd.CmdCert, + cmd.CmdGenerate, + cmd.CmdDocs, } - // default configuration flags + // shared configuration flags, they are for global and for each sub-command at the same time + // eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed + // keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore. globalFlags := []cli.Flag{ cli.HelpFlag, cli.StringFlag{ @@ -126,13 +136,15 @@ func main() { // Set the default to be equivalent to cmdWeb and add the default flags app.Flags = append(app.Flags, globalFlags...) - app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) + app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) // TODO: the web flags polluted the global flags, they are not really global flags app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action) app.HideHelp = true // use our own help action to show helps (with more information like default config) - app.Commands = append(app.Commands, cmdHelp) - for i := range app.Commands { - prepareSubcommands(&app.Commands[i], globalFlags) + app.Before = cmd.PrepareConsoleLoggerLevel(log.INFO) + for i := range subCmdWithIni { + prepareSubcommands(&subCmdWithIni[i], globalFlags) } + app.Commands = append(app.Commands, subCmdWithIni...) + app.Commands = append(app.Commands, subCmdStandalone...) err := app.Run(os.Args) if err != nil { @@ -156,11 +168,7 @@ func prepareSubcommands(command *cli.Command, defaultFlags []cli.Flag) { // prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config // It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times -func prepareWorkPathAndCustomConf(a any) func(ctx *cli.Context) error { - if a == nil { - return nil - } - action := a.(func(*cli.Context) error) +func prepareWorkPathAndCustomConf(action any) func(ctx *cli.Context) error { return func(ctx *cli.Context) error { var args setting.ArgWorkPathAndCustomConf curCtx := ctx @@ -177,10 +185,11 @@ func prepareWorkPathAndCustomConf(a any) func(ctx *cli.Context) error { curCtx = curCtx.Parent() } setting.InitWorkPathAndCommonConfig(os.Getenv, args) - if ctx.Bool("help") { + if ctx.Bool("help") || action == nil { + // the default behavior of "urfave/cli": "nil action" means "show help" return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx) } - return action(ctx) + return action.(func(*cli.Context) error)(ctx) } } diff --git a/models/actions/run.go b/models/actions/run.go index 0654809900c6..7b62ff884f4c 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -36,12 +36,13 @@ type ActionRun struct { TriggerUser *user_model.User `xorm:"-"` Ref string CommitSHA string - IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. - NeedApproval bool // may need approval if it's a fork pull request - ApprovedBy int64 `xorm:"index"` // who approved - Event webhook_module.HookEventType - EventPayload string `xorm:"LONGTEXT"` - Status Status `xorm:"index"` + IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. + NeedApproval bool // may need approval if it's a fork pull request + ApprovedBy int64 `xorm:"index"` // who approved + Event webhook_module.HookEventType // the webhook event that causes the workflow to run + EventPayload string `xorm:"LONGTEXT"` + TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow + Status Status `xorm:"index"` Started timeutil.TimeStamp Stopped timeutil.TimeStamp Created timeutil.TimeStamp `xorm:"created"` diff --git a/models/actions/task.go b/models/actions/task.go index 79b1d46dd0d5..719fd193657f 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -344,6 +344,9 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { return err } +// UpdateTaskByState updates the task by the state. +// It will always update the task if the state is not final, even there is no change. +// So it will update ActionTask.Updated to avoid the task being judged as a zombie task. func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionTask, error) { stepStates := map[int64]*runnerv1.StepState{} for _, v := range state.Steps { @@ -384,6 +387,12 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT }, nil); err != nil { return nil, err } + } else { + // Force update ActionTask.Updated to avoid the task being judged as a zombie task + task.Updated = timeutil.TimeStampNow() + if err := UpdateTask(ctx, task, "updated"); err != nil { + return nil, err + } } if err := task.LoadAttributes(ctx); err != nil { diff --git a/models/db/engine.go b/models/db/engine.go index 56dd209fd78f..3eb16f8042dd 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -123,7 +123,10 @@ func newXORMEngine() (*xorm.Engine, error) { // SyncAllTables sync the schemas of all tables, is required by unit test code func SyncAllTables() error { - return x.StoreEngine("InnoDB").Sync2(tables...) + _, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ + WarnIfDatabaseColumnMissed: true, + }, tables...) + return err } // InitEngine initializes the xorm.Engine and sets it as db.DefaultContext diff --git a/models/db/search.go b/models/db/search.go index 105cb64c4151..aa577f08e043 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -20,6 +20,10 @@ const ( SearchOrderByNewest SearchOrderBy = "created_unix DESC" SearchOrderBySize SearchOrderBy = "size ASC" SearchOrderBySizeReverse SearchOrderBy = "size DESC" + SearchOrderByGitSize SearchOrderBy = "git_size ASC" + SearchOrderByGitSizeReverse SearchOrderBy = "git_size DESC" + SearchOrderByLFSSize SearchOrderBy = "lfs_size ASC" + SearchOrderByLFSSizeReverse SearchOrderBy = "lfs_size DESC" SearchOrderByID SearchOrderBy = "id ASC" SearchOrderByIDReverse SearchOrderBy = "id DESC" SearchOrderByStars SearchOrderBy = "num_stars ASC" diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go index bac1cb9eb608..3650ce057e4c 100644 --- a/models/dbfs/dbfile.go +++ b/models/dbfs/dbfile.go @@ -7,6 +7,7 @@ import ( "context" "errors" "io" + "io/fs" "os" "path/filepath" "strconv" @@ -21,6 +22,7 @@ var defaultFileBlockSize int64 = 32 * 1024 type File interface { io.ReadWriteCloser io.Seeker + fs.File } type file struct { @@ -193,10 +195,26 @@ func (f *file) Close() error { return nil } +func (f *file) Stat() (os.FileInfo, error) { + if f.metaID == 0 { + return nil, os.ErrInvalid + } + + fileMeta, err := findFileMetaByID(f.ctx, f.metaID) + if err != nil { + return nil, err + } + return fileMeta, nil +} + func timeToFileTimestamp(t time.Time) int64 { return t.UnixMicro() } +func fileTimestampToTime(timestamp int64) time.Time { + return time.UnixMicro(timestamp) +} + func (f *file) loadMetaByPath() (*dbfsMeta, error) { var fileMeta dbfsMeta if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil { diff --git a/models/dbfs/dbfs.go b/models/dbfs/dbfs.go index 6b5b3beeb274..f68b4a2b70b4 100644 --- a/models/dbfs/dbfs.go +++ b/models/dbfs/dbfs.go @@ -5,7 +5,10 @@ package dbfs import ( "context" + "io/fs" "os" + "path" + "time" "code.gitea.io/gitea/models/db" ) @@ -100,3 +103,29 @@ func Remove(ctx context.Context, name string) error { defer f.Close() return f.delete() } + +var _ fs.FileInfo = (*dbfsMeta)(nil) + +func (m *dbfsMeta) Name() string { + return path.Base(m.FullPath) +} + +func (m *dbfsMeta) Size() int64 { + return m.FileSize +} + +func (m *dbfsMeta) Mode() fs.FileMode { + return os.ModePerm +} + +func (m *dbfsMeta) ModTime() time.Time { + return fileTimestampToTime(m.ModifyTimestamp) +} + +func (m *dbfsMeta) IsDir() bool { + return false +} + +func (m *dbfsMeta) Sys() any { + return nil +} diff --git a/models/dbfs/dbfs_test.go b/models/dbfs/dbfs_test.go index 300758c623ad..96cb1014c71f 100644 --- a/models/dbfs/dbfs_test.go +++ b/models/dbfs/dbfs_test.go @@ -111,6 +111,19 @@ func TestDbfsBasic(t *testing.T) { _, err = OpenFile(db.DefaultContext, "test2.txt", os.O_RDONLY) assert.Error(t, err) + + // test stat + f, err = OpenFile(db.DefaultContext, "test/test.txt", os.O_RDWR|os.O_CREATE) + assert.NoError(t, err) + stat, err := f.Stat() + assert.NoError(t, err) + assert.EqualValues(t, "test.txt", stat.Name()) + assert.EqualValues(t, 0, stat.Size()) + _, err = f.Write([]byte("0123456789")) + assert.NoError(t, err) + stat, err = f.Stat() + assert.NoError(t, err) + assert.EqualValues(t, 10, stat.Size()) } func TestDbfsReadWrite(t *testing.T) { diff --git a/models/error.go b/models/error.go index 8223f23585bb..b7bb967b739f 100644 --- a/models/error.go +++ b/models/error.go @@ -318,90 +318,6 @@ func (err ErrFilePathProtected) Unwrap() error { return util.ErrPermissionDenied } -// __________ .__ -// \______ \____________ ____ ____ | |__ -// | | _/\_ __ \__ \ / \_/ ___\| | \ -// | | \ | | \// __ \| | \ \___| Y \ -// |______ / |__| (____ /___| /\___ >___| / -// \/ \/ \/ \/ \/ - -// ErrBranchDoesNotExist represents an error that branch with such name does not exist. -type ErrBranchDoesNotExist struct { - BranchName string -} - -// IsErrBranchDoesNotExist checks if an error is an ErrBranchDoesNotExist. -func IsErrBranchDoesNotExist(err error) bool { - _, ok := err.(ErrBranchDoesNotExist) - return ok -} - -func (err ErrBranchDoesNotExist) Error() string { - return fmt.Sprintf("branch does not exist [name: %s]", err.BranchName) -} - -func (err ErrBranchDoesNotExist) Unwrap() error { - return util.ErrNotExist -} - -// ErrBranchAlreadyExists represents an error that branch with such name already exists. -type ErrBranchAlreadyExists struct { - BranchName string -} - -// IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists. -func IsErrBranchAlreadyExists(err error) bool { - _, ok := err.(ErrBranchAlreadyExists) - return ok -} - -func (err ErrBranchAlreadyExists) Error() string { - return fmt.Sprintf("branch already exists [name: %s]", err.BranchName) -} - -func (err ErrBranchAlreadyExists) Unwrap() error { - return util.ErrAlreadyExist -} - -// ErrBranchNameConflict represents an error that branch name conflicts with other branch. -type ErrBranchNameConflict struct { - BranchName string -} - -// IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict. -func IsErrBranchNameConflict(err error) bool { - _, ok := err.(ErrBranchNameConflict) - return ok -} - -func (err ErrBranchNameConflict) Error() string { - return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName) -} - -func (err ErrBranchNameConflict) Unwrap() error { - return util.ErrAlreadyExist -} - -// ErrBranchesEqual represents an error that branch name conflicts with other branch. -type ErrBranchesEqual struct { - BaseBranchName string - HeadBranchName string -} - -// IsErrBranchesEqual checks if an error is an ErrBranchesEqual. -func IsErrBranchesEqual(err error) bool { - _, ok := err.(ErrBranchesEqual) - return ok -} - -func (err ErrBranchesEqual) Error() string { - return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName) -} - -func (err ErrBranchesEqual) Unwrap() error { - return util.ErrInvalidArgument -} - // ErrDisallowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it. type ErrDisallowedToMerge struct { Reason string diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml new file mode 100644 index 000000000000..93003049c67f --- /dev/null +++ b/models/fixtures/branch.yml @@ -0,0 +1,47 @@ +- + id: 1 + repo_id: 1 + name: 'foo' + commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' + commit_message: 'first commit' + commit_time: 978307100 + pusher_id: 1 + is_deleted: true + deleted_by_id: 1 + deleted_unix: 978307200 + +- + id: 2 + repo_id: 1 + name: 'bar' + commit_id: '62fb502a7172d4453f0322a2cc85bddffa57f07a' + commit_message: 'second commit' + commit_time: 978307100 + pusher_id: 1 + is_deleted: true + deleted_by_id: 99 + deleted_unix: 978307200 + +- + id: 3 + repo_id: 1 + name: 'branch2' + commit_id: '985f0301dba5e7b34be866819cd15ad3d8f508ee' + commit_message: 'make pull5 outdated' + commit_time: 1579166279 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 4 + repo_id: 1 + name: 'master' + commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d' + commit_message: 'Initial commit' + commit_time: 1489927679 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/models/fixtures/deleted_branch.yml b/models/fixtures/deleted_branch.yml deleted file mode 100644 index 6a08a78343b5..000000000000 --- a/models/fixtures/deleted_branch.yml +++ /dev/null @@ -1,15 +0,0 @@ -- - id: 1 - repo_id: 1 - name: foo - commit: 1213212312313213213132131 - deleted_by_id: 1 - deleted_unix: 978307200 - -- - id: 2 - repo_id: 1 - name: bar - commit: 5655464564554545466464655 - deleted_by_id: 99 - deleted_unix: 978307200 diff --git a/models/fixtures/mirror.yml b/models/fixtures/mirror.yml new file mode 100644 index 000000000000..97bc4ae60dda --- /dev/null +++ b/models/fixtures/mirror.yml @@ -0,0 +1,49 @@ +- + id: 1 + repo_id: 5 + interval: 3600 + enable_prune: false + updated_unix: 0 + next_update_unix: 0 + lfs_enabled: false + lfs_endpoint: "" + +- + id: 2 + repo_id: 25 + interval: 3600 + enable_prune: false + updated_unix: 0 + next_update_unix: 0 + lfs_enabled: false + lfs_endpoint: "" + +- + id: 3 + repo_id: 26 + interval: 3600 + enable_prune: false + updated_unix: 0 + next_update_unix: 0 + lfs_enabled: false + lfs_endpoint: "" + +- + id: 4 + repo_id: 27 + interval: 3600 + enable_prune: false + updated_unix: 0 + next_update_unix: 0 + lfs_enabled: false + lfs_endpoint: "" + +- + id: 5 + repo_id: 28 + interval: 3600 + enable_prune: false + updated_unix: 0 + next_update_unix: 0 + lfs_enabled: false + lfs_endpoint: "" diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index ef7730780f74..050a9e2d06f4 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -141,7 +141,7 @@ num_projects: 0 num_closed_projects: 0 is_private: true - is_empty: true + is_empty: false is_archived: false is_mirror: true status: 0 diff --git a/models/git/branch.go b/models/git/branch.go new file mode 100644 index 000000000000..88ed858b1939 --- /dev/null +++ b/models/git/branch.go @@ -0,0 +1,380 @@ +// Copyright 2016 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "fmt" + "time" + + "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/log" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +// ErrBranchNotExist represents an error that branch with such name does not exist. +type ErrBranchNotExist struct { + RepoID int64 + BranchName string +} + +// IsErrBranchNotExist checks if an error is an ErrBranchDoesNotExist. +func IsErrBranchNotExist(err error) bool { + _, ok := err.(ErrBranchNotExist) + return ok +} + +func (err ErrBranchNotExist) Error() string { + return fmt.Sprintf("branch does not exist [repo_id: %d name: %s]", err.RepoID, err.BranchName) +} + +func (err ErrBranchNotExist) Unwrap() error { + return util.ErrNotExist +} + +// ErrBranchAlreadyExists represents an error that branch with such name already exists. +type ErrBranchAlreadyExists struct { + BranchName string +} + +// IsErrBranchAlreadyExists checks if an error is an ErrBranchAlreadyExists. +func IsErrBranchAlreadyExists(err error) bool { + _, ok := err.(ErrBranchAlreadyExists) + return ok +} + +func (err ErrBranchAlreadyExists) Error() string { + return fmt.Sprintf("branch already exists [name: %s]", err.BranchName) +} + +func (err ErrBranchAlreadyExists) Unwrap() error { + return util.ErrAlreadyExist +} + +// ErrBranchNameConflict represents an error that branch name conflicts with other branch. +type ErrBranchNameConflict struct { + BranchName string +} + +// IsErrBranchNameConflict checks if an error is an ErrBranchNameConflict. +func IsErrBranchNameConflict(err error) bool { + _, ok := err.(ErrBranchNameConflict) + return ok +} + +func (err ErrBranchNameConflict) Error() string { + return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName) +} + +func (err ErrBranchNameConflict) Unwrap() error { + return util.ErrAlreadyExist +} + +// ErrBranchesEqual represents an error that base branch is equal to the head branch. +type ErrBranchesEqual struct { + BaseBranchName string + HeadBranchName string +} + +// IsErrBranchesEqual checks if an error is an ErrBranchesEqual. +func IsErrBranchesEqual(err error) bool { + _, ok := err.(ErrBranchesEqual) + return ok +} + +func (err ErrBranchesEqual) Error() string { + return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName) +} + +func (err ErrBranchesEqual) Unwrap() error { + return util.ErrInvalidArgument +} + +// Branch represents a branch of a repository +// For those repository who have many branches, stored into database is a good choice +// for pagination, keyword search and filtering +type Branch struct { + ID int64 + RepoID int64 `xorm:"UNIQUE(s)"` + Name string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment + CommitID string + CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line) + PusherID int64 + Pusher *user_model.User `xorm:"-"` + IsDeleted bool `xorm:"index"` + DeletedByID int64 + DeletedBy *user_model.User `xorm:"-"` + DeletedUnix timeutil.TimeStamp `xorm:"index"` + CommitTime timeutil.TimeStamp // The commit + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) { + if b.DeletedBy == nil { + b.DeletedBy, err = user_model.GetUserByID(ctx, b.DeletedByID) + if user_model.IsErrUserNotExist(err) { + b.DeletedBy = user_model.NewGhostUser() + err = nil + } + } + return err +} + +func (b *Branch) LoadPusher(ctx context.Context) (err error) { + if b.Pusher == nil && b.PusherID > 0 { + b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID) + if user_model.IsErrUserNotExist(err) { + b.Pusher = user_model.NewGhostUser() + err = nil + } + } + return err +} + +func init() { + db.RegisterModel(new(Branch)) + db.RegisterModel(new(RenamedBranch)) +} + +func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, error) { + var branch Branch + has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch) + if err != nil { + return nil, err + } else if !has { + return nil, ErrBranchNotExist{ + RepoID: repoID, + BranchName: branchName, + } + } + return &branch, nil +} + +func AddBranches(ctx context.Context, branches []*Branch) error { + for _, branch := range branches { + if _, err := db.GetEngine(ctx).Insert(branch); err != nil { + return err + } + } + return nil +} + +func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch, error) { + var branch Branch + has, err := db.GetEngine(ctx).ID(branchID).Get(&branch) + if err != nil { + return nil, err + } else if !has { + return nil, ErrBranchNotExist{ + RepoID: repoID, + } + } + if branch.RepoID != repoID { + return nil, ErrBranchNotExist{ + RepoID: repoID, + } + } + if !branch.IsDeleted { + return nil, ErrBranchNotExist{ + RepoID: repoID, + } + } + return &branch, nil +} + +func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + branches := make([]*Branch, 0, len(branchIDs)) + if err := db.GetEngine(ctx).In("id", branchIDs).Find(&branches); err != nil { + return err + } + for _, branch := range branches { + if err := AddDeletedBranch(ctx, repoID, branch.Name, doerID); err != nil { + return err + } + } + return nil + }) +} + +// UpdateBranch updates the branch information in the database. If the branch exist, it will update latest commit of this branch information +// If it doest not exist, insert a new record into database +func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error { + cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName). + Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix"). + Update(&Branch{ + CommitID: commit.ID.String(), + CommitMessage: commit.Summary(), + PusherID: pusherID, + CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), + IsDeleted: false, + }) + if err != nil { + return err + } + if cnt > 0 { + return nil + } + + return db.Insert(ctx, &Branch{ + RepoID: repoID, + Name: branchName, + CommitID: commit.ID.String(), + CommitMessage: commit.Summary(), + PusherID: pusherID, + CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), + }) +} + +// AddDeletedBranch adds a deleted branch to the database +func AddDeletedBranch(ctx context.Context, repoID int64, branchName string, deletedByID int64) error { + branch, err := GetBranch(ctx, repoID, branchName) + if err != nil { + return err + } + if branch.IsDeleted { + return nil + } + + cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=? AND is_deleted=?", repoID, branchName, false). + Cols("is_deleted, deleted_by_id, deleted_unix"). + Update(&Branch{ + IsDeleted: true, + DeletedByID: deletedByID, + DeletedUnix: timeutil.TimeStampNow(), + }) + if err != nil { + return err + } + if cnt == 0 { + return fmt.Errorf("branch %s not found or has been deleted", branchName) + } + return err +} + +func RemoveDeletedBranchByID(ctx context.Context, repoID, branchID int64) error { + _, err := db.GetEngine(ctx).Where("repo_id=? AND id=? AND is_deleted = ?", repoID, branchID, true).Delete(new(Branch)) + return err +} + +// RemoveOldDeletedBranches removes old deleted branches +func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { + // Nothing to do for shutdown or terminate + log.Trace("Doing: DeletedBranchesCleanup") + + deleteBefore := time.Now().Add(-olderThan) + _, err := db.GetEngine(ctx).Where("is_deleted=? AND deleted_unix < ?", true, deleteBefore.Unix()).Delete(new(Branch)) + if err != nil { + log.Error("DeletedBranchesCleanup: %v", err) + } +} + +// RenamedBranch provide renamed branch log +// will check it when a branch can't be found +type RenamedBranch struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX NOT NULL"` + From string + To string + CreatedUnix timeutil.TimeStamp `xorm:"created"` +} + +// FindRenamedBranch check if a branch was renamed +func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) { + branch = &RenamedBranch{ + RepoID: repoID, + From: from, + } + exist, err = db.GetEngine(ctx).Get(branch) + + return branch, exist, err +} + +// RenameBranch rename a branch +func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + // 1. update branch in database + if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{ + Name: to, + }); err != nil { + return err + } else if n <= 0 { + return ErrBranchNotExist{ + RepoID: repo.ID, + BranchName: from, + } + } + + // 2. update default branch if needed + isDefault := repo.DefaultBranch == from + if isDefault { + repo.DefaultBranch = to + _, err = sess.ID(repo.ID).Cols("default_branch").Update(repo) + if err != nil { + return err + } + } + + // 3. Update protected branch if needed + protectedBranch, err := GetProtectedBranchRuleByName(ctx, repo.ID, from) + if err != nil { + return err + } + + if protectedBranch != nil { + // there is a protect rule for this branch + protectedBranch.RuleName = to + _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch) + if err != nil { + return err + } + } else { + // some glob protect rules may match this branch + protected, err := IsBranchProtected(ctx, repo.ID, from) + if err != nil { + return err + } + if protected { + return ErrBranchIsProtected + } + } + + // 4. Update all not merged pull request base branch name + _, err = sess.Table("pull_request").Where("base_repo_id=? AND base_branch=? AND has_merged=?", + repo.ID, from, false). + Update(map[string]interface{}{"base_branch": to}) + if err != nil { + return err + } + + // 5. do git action + if err = gitAction(isDefault); err != nil { + return err + } + + // 6. insert renamed branch record + renamedBranch := &RenamedBranch{ + RepoID: repo.ID, + From: from, + To: to, + } + err = db.Insert(ctx, renamedBranch) + if err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/git/branch_list.go b/models/git/branch_list.go new file mode 100644 index 000000000000..da78248c0bc0 --- /dev/null +++ b/models/git/branch_list.go @@ -0,0 +1,132 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm" +) + +type BranchList []*Branch + +func (branches BranchList) LoadDeletedBy(ctx context.Context) error { + ids := container.Set[int64]{} + for _, branch := range branches { + if !branch.IsDeleted { + continue + } + ids.Add(branch.DeletedByID) + } + usersMap := make(map[int64]*user_model.User, len(ids)) + if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil { + return err + } + for _, branch := range branches { + if !branch.IsDeleted { + continue + } + branch.DeletedBy = usersMap[branch.DeletedByID] + if branch.DeletedBy == nil { + branch.DeletedBy = user_model.NewGhostUser() + } + } + return nil +} + +func (branches BranchList) LoadPusher(ctx context.Context) error { + ids := container.Set[int64]{} + for _, branch := range branches { + if branch.PusherID > 0 { // pusher_id maybe zero because some branches are sync by backend with no pusher + ids.Add(branch.PusherID) + } + } + usersMap := make(map[int64]*user_model.User, len(ids)) + if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil { + return err + } + for _, branch := range branches { + if branch.PusherID <= 0 { + continue + } + branch.Pusher = usersMap[branch.PusherID] + if branch.Pusher == nil { + branch.Pusher = user_model.NewGhostUser() + } + } + return nil +} + +const ( + BranchOrderByNameAsc = "name ASC" + BranchOrderByCommitTimeDesc = "commit_time DESC" +) + +type FindBranchOptions struct { + db.ListOptions + RepoID int64 + ExcludeBranchNames []string + IsDeletedBranch util.OptionalBool + OrderBy string +} + +func (opts *FindBranchOptions) Cond() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + + if len(opts.ExcludeBranchNames) > 0 { + cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames)) + } + if !opts.IsDeletedBranch.IsNone() { + cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.IsTrue()}) + } + return cond +} + +func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) { + return db.GetEngine(ctx).Where(opts.Cond()).Count(&Branch{}) +} + +func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session { + if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end + sess = sess.OrderBy("is_deleted ASC") + } + + if opts.OrderBy == "" { + opts.OrderBy = BranchOrderByCommitTimeDesc + } + return sess.OrderBy(opts.OrderBy) +} + +func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) { + sess := db.GetEngine(ctx).Where(opts.Cond()) + if opts.PageSize > 0 && !opts.IsListAll() { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + sess = orderByBranches(sess, opts) + + var branches []*Branch + return branches, sess.Find(&branches) +} + +func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) { + sess := db.GetEngine(ctx).Select("name").Where(opts.Cond()) + if opts.PageSize > 0 && !opts.IsListAll() { + sess = db.SetSessionPagination(sess, &opts.ListOptions) + } + sess = orderByBranches(sess, opts) + var branches []string + if err := sess.Table("branch").Find(&branches); err != nil { + return nil, err + } + return branches, nil +} diff --git a/models/git/branches_test.go b/models/git/branch_test.go similarity index 74% rename from models/git/branches_test.go rename to models/git/branch_test.go index 5d18d9525ef8..ba6902692792 100644 --- a/models/git/branches_test.go +++ b/models/git/branch_test.go @@ -11,6 +11,8 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -18,24 +20,45 @@ import ( func TestAddDeletedBranch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) - assert.Error(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, firstBranch.Name, firstBranch.Commit, firstBranch.DeletedByID)) - assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, "test", "5655464564554545466464656", int64(1))) + assert.True(t, firstBranch.IsDeleted) + assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, firstBranch.Name, firstBranch.DeletedByID)) + assert.NoError(t, git_model.AddDeletedBranch(db.DefaultContext, repo.ID, "branch2", int64(1))) + + secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: "branch2"}) + assert.True(t, secondBranch.IsDeleted) + + commit := &git.Commit{ + ID: git.MustIDFromString(secondBranch.CommitID), + CommitMessage: secondBranch.CommitMessage, + Committer: &git.Signature{ + When: secondBranch.CommitTime.AsLocalTime(), + }, + } + + err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit) + assert.NoError(t, err) } func TestGetDeletedBranches(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - branches, err := git_model.GetDeletedBranches(db.DefaultContext, repo.ID) + branches, err := git_model.FindBranches(db.DefaultContext, git_model.FindBranchOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + RepoID: repo.ID, + IsDeletedBranch: util.OptionalBoolTrue, + }) assert.NoError(t, err) assert.Len(t, branches, 2) } func TestGetDeletedBranch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) assert.NotNil(t, getDeletedBranch(t, firstBranch)) } @@ -43,18 +66,18 @@ func TestGetDeletedBranch(t *testing.T) { func TestDeletedBranchLoadUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) - secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 2}) + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) + secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 2}) branch := getDeletedBranch(t, firstBranch) assert.Nil(t, branch.DeletedBy) - branch.LoadUser(db.DefaultContext) + branch.LoadDeletedBy(db.DefaultContext) assert.NotNil(t, branch.DeletedBy) assert.Equal(t, "user1", branch.DeletedBy.Name) branch = getDeletedBranch(t, secondBranch) assert.Nil(t, branch.DeletedBy) - branch.LoadUser(db.DefaultContext) + branch.LoadDeletedBy(db.DefaultContext) assert.NotNil(t, branch.DeletedBy) assert.Equal(t, "Ghost", branch.DeletedBy.Name) } @@ -63,22 +86,22 @@ func TestRemoveDeletedBranch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 1}) + firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) err := git_model.RemoveDeletedBranchByID(db.DefaultContext, repo.ID, 1) assert.NoError(t, err) unittest.AssertNotExistsBean(t, firstBranch) - unittest.AssertExistsAndLoadBean(t, &git_model.DeletedBranch{ID: 2}) + unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 2}) } -func getDeletedBranch(t *testing.T, branch *git_model.DeletedBranch) *git_model.DeletedBranch { +func getDeletedBranch(t *testing.T, branch *git_model.Branch) *git_model.Branch { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo.ID, branch.ID) assert.NoError(t, err) assert.Equal(t, branch.ID, deletedBranch.ID) assert.Equal(t, branch.Name, deletedBranch.Name) - assert.Equal(t, branch.Commit, deletedBranch.Commit) + assert.Equal(t, branch.CommitID, deletedBranch.CommitID) assert.Equal(t, branch.DeletedByID, deletedBranch.DeletedByID) return deletedBranch @@ -146,8 +169,8 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { deletedBranch, err := git_model.GetDeletedBranchByID(db.DefaultContext, repo2.ID, 1) - // Expect no error, and the returned branch is nil. - assert.NoError(t, err) + // Expect error, and the returned branch is nil. + assert.Error(t, err) assert.Nil(t, deletedBranch) // Now get the deletedBranch with ID of 1 on repo with ID 1. diff --git a/models/git/branches.go b/models/git/branches.go deleted file mode 100644 index b94ea329599a..000000000000 --- a/models/git/branches.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2016 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "context" - "fmt" - "time" - - "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/log" - "code.gitea.io/gitea/modules/timeutil" -) - -// DeletedBranch struct -type DeletedBranch struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string `xorm:"UNIQUE(s) NOT NULL"` - Commit string `xorm:"UNIQUE(s) NOT NULL"` - DeletedByID int64 `xorm:"INDEX"` - DeletedBy *user_model.User `xorm:"-"` - DeletedUnix timeutil.TimeStamp `xorm:"INDEX created"` -} - -func init() { - db.RegisterModel(new(DeletedBranch)) - db.RegisterModel(new(RenamedBranch)) -} - -// AddDeletedBranch adds a deleted branch to the database -func AddDeletedBranch(ctx context.Context, repoID int64, branchName, commit string, deletedByID int64) error { - deletedBranch := &DeletedBranch{ - RepoID: repoID, - Name: branchName, - Commit: commit, - DeletedByID: deletedByID, - } - - _, err := db.GetEngine(ctx).Insert(deletedBranch) - return err -} - -// GetDeletedBranches returns all the deleted branches -func GetDeletedBranches(ctx context.Context, repoID int64) ([]*DeletedBranch, error) { - deletedBranches := make([]*DeletedBranch, 0) - return deletedBranches, db.GetEngine(ctx).Where("repo_id = ?", repoID).Desc("deleted_unix").Find(&deletedBranches) -} - -// GetDeletedBranchByID get a deleted branch by its ID -func GetDeletedBranchByID(ctx context.Context, repoID, id int64) (*DeletedBranch, error) { - deletedBranch := &DeletedBranch{} - has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).And("id = ?", id).Get(deletedBranch) - if err != nil { - return nil, err - } - if !has { - return nil, nil - } - return deletedBranch, nil -} - -// RemoveDeletedBranchByID removes a deleted branch from the database -func RemoveDeletedBranchByID(ctx context.Context, repoID, id int64) (err error) { - deletedBranch := &DeletedBranch{ - RepoID: repoID, - ID: id, - } - - if affected, err := db.GetEngine(ctx).Delete(deletedBranch); err != nil { - return err - } else if affected != 1 { - return fmt.Errorf("remove deleted branch ID(%v) failed", id) - } - - return nil -} - -// LoadUser loads the user that deleted the branch -// When there's no user found it returns a user_model.NewGhostUser -func (deletedBranch *DeletedBranch) LoadUser(ctx context.Context) { - user, err := user_model.GetUserByID(ctx, deletedBranch.DeletedByID) - if err != nil { - user = user_model.NewGhostUser() - } - deletedBranch.DeletedBy = user -} - -// RemoveDeletedBranchByName removes all deleted branches -func RemoveDeletedBranchByName(ctx context.Context, repoID int64, branch string) error { - _, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branch).Delete(new(DeletedBranch)) - return err -} - -// RemoveOldDeletedBranches removes old deleted branches -func RemoveOldDeletedBranches(ctx context.Context, olderThan time.Duration) { - // Nothing to do for shutdown or terminate - log.Trace("Doing: DeletedBranchesCleanup") - - deleteBefore := time.Now().Add(-olderThan) - _, err := db.GetEngine(ctx).Where("deleted_unix < ?", deleteBefore.Unix()).Delete(new(DeletedBranch)) - if err != nil { - log.Error("DeletedBranchesCleanup: %v", err) - } -} - -// RenamedBranch provide renamed branch log -// will check it when a branch can't be found -type RenamedBranch struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX NOT NULL"` - From string - To string - CreatedUnix timeutil.TimeStamp `xorm:"created"` -} - -// FindRenamedBranch check if a branch was renamed -func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *RenamedBranch, exist bool, err error) { - branch = &RenamedBranch{ - RepoID: repoID, - From: from, - } - exist, err = db.GetEngine(ctx).Get(branch) - - return branch, exist, err -} - -// RenameBranch rename a branch -func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - sess := db.GetEngine(ctx) - // 1. update default branch if needed - isDefault := repo.DefaultBranch == from - if isDefault { - repo.DefaultBranch = to - _, err = sess.ID(repo.ID).Cols("default_branch").Update(repo) - if err != nil { - return err - } - } - - // 2. Update protected branch if needed - protectedBranch, err := GetProtectedBranchRuleByName(ctx, repo.ID, from) - if err != nil { - return err - } - - if protectedBranch != nil { - protectedBranch.RuleName = to - _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch) - if err != nil { - return err - } - } else { - protected, err := IsBranchProtected(ctx, repo.ID, from) - if err != nil { - return err - } - if protected { - return ErrBranchIsProtected - } - } - - // 3. Update all not merged pull request base branch name - _, err = sess.Table("pull_request").Where("base_repo_id=? AND base_branch=? AND has_merged=?", - repo.ID, from, false). - Update(map[string]interface{}{"base_branch": to}) - if err != nil { - return err - } - - // 4. do git action - if err = gitAction(isDefault); err != nil { - return err - } - - // 5. insert renamed branch record - renamedBranch := &RenamedBranch{ - RepoID: repo.ID, - From: from, - To: to, - } - err = db.Insert(ctx, renamedBranch) - if err != nil { - return err - } - - return committer.Commit() -} diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index 17fe6d701fb4..eeb307e2454e 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -8,7 +8,7 @@ import ( "sort" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" "github.com/gobwas/glob" ) @@ -47,19 +47,32 @@ func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedB } // FindAllMatchedBranches find all matched branches -func FindAllMatchedBranches(ctx context.Context, gitRepo *git.Repository, ruleName string) ([]string, error) { - // FIXME: how many should we get? - branches, _, err := gitRepo.GetBranchNames(0, 9999999) - if err != nil { - return nil, err - } - rule := glob.MustCompile(ruleName) - results := make([]string, 0, len(branches)) - for _, branch := range branches { - if rule.Match(branch) { - results = append(results, branch) +func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) ([]string, error) { + results := make([]string, 0, 10) + for page := 1; ; page++ { + brancheNames, err := FindBranchNames(ctx, FindBranchOptions{ + ListOptions: db.ListOptions{ + PageSize: 100, + Page: page, + }, + RepoID: repoID, + IsDeletedBranch: util.OptionalBoolFalse, + }) + if err != nil { + return nil, err + } + rule := glob.MustCompile(ruleName) + + for _, branch := range brancheNames { + if rule.Match(branch) { + results = append(results, branch) + } + } + if len(brancheNames) < 100 { + break } } + return results, nil } diff --git a/models/issues/comment.go b/models/issues/comment.go index e5c90f265e1f..dbe4434ca754 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -167,7 +167,7 @@ func AsCommentType(typeName string) CommentType { func (t CommentType) HasContentSupport() bool { switch t { - case CommentTypeComment, CommentTypeCode, CommentTypeReview: + case CommentTypeComment, CommentTypeCode, CommentTypeReview, CommentTypeDismissReview: return true } return false diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 304ac4569f86..d447d7542cd2 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -18,11 +18,11 @@ import ( type CodeComments map[string]map[int64][]*Comment // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line -func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User) (CodeComments, error) { - return fetchCodeCommentsByReview(ctx, issue, currentUser, nil) +func FetchCodeComments(ctx context.Context, issue *Issue, currentUser *user_model.User, showOutdatedComments bool) (CodeComments, error) { + return fetchCodeCommentsByReview(ctx, issue, currentUser, nil, showOutdatedComments) } -func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review) (CodeComments, error) { +func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) (CodeComments, error) { pathToLineToComment := make(CodeComments) if review == nil { review = &Review{ID: 0} @@ -33,7 +33,7 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u ReviewID: review.ID, } - comments, err := findCodeComments(ctx, opts, issue, currentUser, review) + comments, err := findCodeComments(ctx, opts, issue, currentUser, review, showOutdatedComments) if err != nil { return nil, err } @@ -47,15 +47,17 @@ func fetchCodeCommentsByReview(ctx context.Context, issue *Issue, currentUser *u return pathToLineToComment, nil } -func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review) ([]*Comment, error) { +func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issue, currentUser *user_model.User, review *Review, showOutdatedComments bool) ([]*Comment, error) { var comments CommentList if review == nil { review = &Review{ID: 0} } conds := opts.ToConds() - if review.ID == 0 { + + if !showOutdatedComments && review.ID == 0 { conds = conds.And(builder.Eq{"invalidated": false}) } + e := db.GetEngine(ctx) if err := e.Where(conds). Asc("comment.created_unix"). @@ -118,12 +120,12 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number -func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64) ([]*Comment, error) { +func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) { opts := FindCommentsOptions{ Type: CommentTypeCode, IssueID: issue.ID, TreePath: treePath, Line: line, } - return findCodeComments(ctx, opts, issue, currentUser, nil) + return findCodeComments(ctx, opts, issue, currentUser, nil, showOutdatedComments) } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index 43bad1660f1f..d766625be3dc 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -50,7 +50,7 @@ func TestFetchCodeComments(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user) + res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user, false) assert.NoError(t, err) assert.Contains(t, res, "README.md") assert.Contains(t, res["README.md"], int64(4)) @@ -58,7 +58,7 @@ func TestFetchCodeComments(t *testing.T) { assert.Equal(t, int64(4), res["README.md"][4][0].ID) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2) + res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2, false) assert.NoError(t, err) assert.Len(t, res, 1) } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index dad21c14776f..9cc41ec6ab37 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -229,39 +229,41 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { return nil } -func (issues IssueList) getProjectIDs() []int64 { - ids := make(container.Set[int64], len(issues)) - for _, issue := range issues { - ids.Add(issue.ProjectID()) - } - return ids.Values() -} +func (issues IssueList) LoadProjects(ctx context.Context) error { + issueIDs := issues.getIssueIDs() + projectMaps := make(map[int64]*project_model.Project, len(issues)) + left := len(issueIDs) -func (issues IssueList) loadProjects(ctx context.Context) error { - projectIDs := issues.getProjectIDs() - if len(projectIDs) == 0 { - return nil + type projectWithIssueID struct { + *project_model.Project `xorm:"extends"` + IssueID int64 } - projectMaps := make(map[int64]*project_model.Project, len(projectIDs)) - left := len(projectIDs) for left > 0 { limit := db.DefaultMaxInSize if left < limit { limit = left } + + projects := make([]*projectWithIssueID, 0, limit) err := db.GetEngine(ctx). - In("id", projectIDs[:limit]). - Find(&projectMaps) + Table("project"). + Select("project.*, project_issue.issue_id"). + Join("INNER", "project_issue", "project.id = project_issue.project_id"). + In("project_issue.issue_id", issueIDs[:limit]). + Find(&projects) if err != nil { return err } + for _, project := range projects { + projectMaps[project.IssueID] = project.Project + } left -= limit - projectIDs = projectIDs[limit:] + issueIDs = issueIDs[limit:] } for _, issue := range issues { - issue.Project = projectMaps[issue.ProjectID()] + issue.Project = projectMaps[issue.ID] } return nil } @@ -541,7 +543,7 @@ func (issues IssueList) loadAttributes(ctx context.Context) error { return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err) } - if err := issues.loadProjects(ctx); err != nil { + if err := issues.LoadProjects(ctx); err != nil { return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err) } diff --git a/models/issues/issue_list_test.go b/models/issues/issue_list_test.go index 954a20ffe44e..97ce9e43b377 100644 --- a/models/issues/issue_list_test.go +++ b/models/issues/issue_list_test.go @@ -66,8 +66,12 @@ func TestIssueList_LoadAttributes(t *testing.T) { } if issue.ID == int64(1) { assert.Equal(t, int64(400), issue.TotalTrackedTime) + assert.NotNil(t, issue.Project) } else if issue.ID == int64(2) { assert.Equal(t, int64(3682), issue.TotalTrackedTime) + assert.Nil(t, issue.Project) + } else { + assert.Nil(t, issue.Project) } } } diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index 04d12e055cc5..b163c683577f 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -27,11 +27,6 @@ func (issue *Issue) LoadProject(ctx context.Context) (err error) { return err } -// ProjectID return project id if issue was assigned to one -func (issue *Issue) ProjectID() int64 { - return issue.projectID(db.DefaultContext) -} - func (issue *Issue) projectID(ctx context.Context) int64 { var ip project_model.ProjectIssue has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip) diff --git a/models/issues/review.go b/models/issues/review.go index 3a1ab7468afc..3685c65ce581 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -141,7 +141,7 @@ func (r *Review) LoadCodeComments(ctx context.Context) (err error) { if err = r.loadIssue(ctx); err != nil { return } - r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r) + r.CodeComments, err = fetchCodeCommentsByReview(ctx, r.Issue, nil, r, false) return err } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index 698014afeba5..1d7592926bc5 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -6,6 +6,7 @@ package issues import ( "context" "errors" + "fmt" "time" "code.gitea.io/gitea/models/db" @@ -173,10 +174,12 @@ func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Tim } if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - Content: util.SecToTime(amount), + Issue: issue, + Repo: issue.Repo, + Doer: user, + // Content before v1.21 did store the formated string instead of seconds, + // so use "|" as delimeter to mark the new format + Content: fmt.Sprintf("|%d", amount), Type: CommentTypeAddTimeManual, TimeID: t.ID, }); err != nil { @@ -251,10 +254,12 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { return err } if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: issue, - Repo: issue.Repo, - Doer: user, - Content: "- " + util.SecToTime(removedTime), + Issue: issue, + Repo: issue.Repo, + Doer: user, + // Content before v1.21 did store the formated string instead of seconds, + // so use "|" as delimeter to mark the new format + Content: fmt.Sprintf("|%d", removedTime), Type: CommentTypeDeleteTimeManual, }); err != nil { return err @@ -280,10 +285,12 @@ func DeleteTime(t *TrackedTime) error { } if _, err := CreateComment(ctx, &CreateCommentOptions{ - Issue: t.Issue, - Repo: t.Issue.Repo, - Doer: t.User, - Content: "- " + util.SecToTime(t.Time), + Issue: t.Issue, + Repo: t.Issue.Repo, + Doer: t.User, + // Content before v1.21 did store the formated string instead of seconds, + // so use "|" as delimeter to mark the new format + Content: fmt.Sprintf("|%d", t.Time), Type: CommentTypeDeleteTimeManual, }); err != nil { return err diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index baa170b20126..37ba1cfdc490 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -35,7 +35,7 @@ func TestAddTime(t *testing.T) { assert.Equal(t, int64(3661), tt.Time) comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}) - assert.Equal(t, "1 hour 1 minute", comment.Content) + assert.Equal(t, "|3661", comment.Content) } func TestGetTrackedTimes(t *testing.T) { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1d443b3d152c..a15b6e4eec8c 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -503,9 +503,14 @@ var migrations = []Migration{ // v260 -> v261 NewMigration("Drop custom_labels column of action_runner table", v1_21.DropCustomLabelsColumnOfActionRunner), - // v261 -> v262 NewMigration("Add variable table", v1_21.CreateVariableTable), + // v262 -> v263 + NewMigration("Add TriggerEvent to action_run table", v1_21.AddTriggerEventToActionRun), + // v263 -> v264 + NewMigration("Add git_size and lfs_size columns to repository table", v1_21.AddGitSizeAndLFSSizeToRepositoryTable), + // v264 -> v265 + NewMigration("Add branch table", v1_21.AddBranchTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v262.go b/models/migrations/v1_21/v262.go new file mode 100644 index 000000000000..23e900572a22 --- /dev/null +++ b/models/migrations/v1_21/v262.go @@ -0,0 +1,16 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "xorm.io/xorm" +) + +func AddTriggerEventToActionRun(x *xorm.Engine) error { + type ActionRun struct { + TriggerEvent string + } + + return x.Sync(new(ActionRun)) +} diff --git a/models/migrations/v1_21/v263.go b/models/migrations/v1_21/v263.go new file mode 100644 index 000000000000..88a5cb92b49f --- /dev/null +++ b/models/migrations/v1_21/v263.go @@ -0,0 +1,41 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "fmt" + + "xorm.io/xorm" +) + +// AddGitSizeAndLFSSizeToRepositoryTable: add GitSize and LFSSize columns to Repository +func AddGitSizeAndLFSSizeToRepositoryTable(x *xorm.Engine) error { + type Repository struct { + GitSize int64 `xorm:"NOT NULL DEFAULT 0"` + LFSSize int64 `xorm:"NOT NULL DEFAULT 0"` + } + + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + if err := sess.Sync2(new(Repository)); err != nil { + return fmt.Errorf("Sync2: %w", err) + } + + _, err := sess.Exec(`UPDATE repository SET lfs_size=(SELECT SUM(size) FROM lfs_meta_object WHERE lfs_meta_object.repository_id=repository.ID) WHERE EXISTS (SELECT 1 FROM lfs_meta_object WHERE lfs_meta_object.repository_id=repository.ID)`) + if err != nil { + return err + } + + _, err = sess.Exec(`UPDATE repository SET git_size = size - lfs_size`) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/migrations/v1_21/v264.go b/models/migrations/v1_21/v264.go new file mode 100644 index 000000000000..60b7a7acf7d0 --- /dev/null +++ b/models/migrations/v1_21/v264.go @@ -0,0 +1,93 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddBranchTable(x *xorm.Engine) error { + type Branch struct { + ID int64 + RepoID int64 `xorm:"UNIQUE(s)"` + Name string `xorm:"UNIQUE(s) NOT NULL"` + CommitID string + CommitMessage string `xorm:"TEXT"` + PusherID int64 + IsDeleted bool `xorm:"index"` + DeletedByID int64 + DeletedUnix timeutil.TimeStamp `xorm:"index"` + CommitTime timeutil.TimeStamp // The commit + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + if err := x.Sync(new(Branch)); err != nil { + return err + } + + if exist, err := x.IsTableExist("deleted_branches"); err != nil { + return err + } else if !exist { + return nil + } + + type DeletedBranch struct { + ID int64 + RepoID int64 `xorm:"index UNIQUE(s)"` + Name string `xorm:"UNIQUE(s) NOT NULL"` + Commit string + DeletedByID int64 + DeletedUnix timeutil.TimeStamp + } + + var adminUserID int64 + has, err := x.Table("user"). + Select("id"). + Where("is_admin=?", true). + Asc("id"). // Reliably get the admin with the lowest ID. + Get(&adminUserID) + if err != nil { + return err + } else if !has { + return fmt.Errorf("no admin user found") + } + + branches := make([]Branch, 0, 100) + if err := db.Iterate(context.Background(), nil, func(ctx context.Context, deletedBranch *DeletedBranch) error { + branches = append(branches, Branch{ + RepoID: deletedBranch.RepoID, + Name: deletedBranch.Name, + CommitID: deletedBranch.Commit, + PusherID: adminUserID, + IsDeleted: true, + DeletedByID: deletedBranch.DeletedByID, + DeletedUnix: deletedBranch.DeletedUnix, + }) + if len(branches) >= 100 { + _, err := x.Insert(&branches) + if err != nil { + return err + } + branches = branches[:0] + } + return nil + }); err != nil { + return err + } + + if len(branches) > 0 { + if _, err := x.Insert(&branches); err != nil { + return err + } + } + + return x.DropTables("deleted_branches") +} diff --git a/models/repo.go b/models/repo.go index 2e0e8af16c4a..933f7e56a3a0 100644 --- a/models/repo.go +++ b/models/repo.go @@ -147,7 +147,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { &repo_model.Collaboration{RepoID: repoID}, &issues_model.Comment{RefRepoID: repoID}, &git_model.CommitStatus{RepoID: repoID}, - &git_model.DeletedBranch{RepoID: repoID}, + &git_model.Branch{RepoID: repoID}, &git_model.LFSLock{RepoID: repoID}, &repo_model.LanguageStat{RepoID: repoID}, &issues_model.Milestone{RepoID: repoID}, diff --git a/models/repo/repo.go b/models/repo/repo.go index d3e6daa95b53..b7c02057c272 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -163,6 +164,8 @@ type Repository struct { IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"` TemplateID int64 `xorm:"INDEX"` Size int64 `xorm:"NOT NULL DEFAULT 0"` + GitSize int64 `xorm:"NOT NULL DEFAULT 0"` + LFSSize int64 `xorm:"NOT NULL DEFAULT 0"` CodeIndexerStatus *RepoIndexerStatus `xorm:"-"` StatsIndexerStatus *RepoIndexerStatus `xorm:"-"` IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` @@ -196,6 +199,42 @@ func (repo *Repository) SanitizedOriginalURL() string { return u.String() } +// text representations to be returned in SizeDetail.Name +const ( + SizeDetailNameGit = "git" + SizeDetailNameLFS = "lfs" +) + +type SizeDetail struct { + Name string + Size int64 +} + +// SizeDetails forms a struct with various size details about repository +func (repo *Repository) SizeDetails() []SizeDetail { + sizeDetails := []SizeDetail{ + { + Name: SizeDetailNameGit, + Size: repo.GitSize, + }, + { + Name: SizeDetailNameLFS, + Size: repo.LFSSize, + }, + } + return sizeDetails +} + +// SizeDetailsString returns a concatenation of all repository size details as a string +func (repo *Repository) SizeDetailsString() string { + var str strings.Builder + sizeDetails := repo.SizeDetails() + for _, detail := range sizeDetails { + str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) + } + return strings.TrimSuffix(str.String(), ", ") +} + func (repo *Repository) LogString() string { if repo == nil { return "" diff --git a/models/repo/update.go b/models/repo/update.go index 4894e0a1b9c5..c4fba32ad205 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -185,9 +185,11 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s } // 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, +func UpdateRepoSize(ctx context.Context, repoID, gitSize, lfsSize int64) error { + _, err := db.GetEngine(ctx).ID(repoID).Cols("size", "git_size", "lfs_size").NoAutoTime().Update(&Repository{ + Size: gitSize + lfsSize, + GitSize: gitSize, + LFSSize: lfsSize, }) return err } diff --git a/models/user/setting_keys.go b/models/user/setting_keys.go index 10255735b317..72b3974eee43 100644 --- a/models/user/setting_keys.go +++ b/models/user/setting_keys.go @@ -8,6 +8,8 @@ const ( SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types" // SettingsKeyDiffWhitespaceBehavior is the setting key for whitespace behavior of diff SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour" + // SettingsKeyShowOutdatedComments is the setting key wether or not to show outdated comments in PRs + SettingsKeyShowOutdatedComments = "comment_code.show_outdated" // UserActivityPubPrivPem is user's private key UserActivityPubPrivPem = "activitypub.priv_pem" // UserActivityPubPubPem is user's public key diff --git a/models/user/user.go b/models/user/user.go index 2077d55f513e..6f9c2f5b35a8 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1171,9 +1171,9 @@ func GetUserByOpenID(uri string) (*User, error) { } // GetAdminUser returns the first administrator -func GetAdminUser() (*User, error) { +func GetAdminUser(ctx context.Context) (*User, error) { var admin User - has, err := db.GetEngine(db.DefaultContext). + has, err := db.GetEngine(ctx). Where("is_admin=?", true). Asc("id"). // Reliably get the admin with the lowest ID. Get(&admin) diff --git a/modules/actions/github.go b/modules/actions/github.go index f3cb335da98c..71f81a89034c 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -8,33 +8,33 @@ import ( ) const ( - githubEventPullRequest = "pull_request" - githubEventPullRequestTarget = "pull_request_target" - githubEventPullRequestReviewComment = "pull_request_review_comment" - githubEventPullRequestReview = "pull_request_review" - githubEventRegistryPackage = "registry_package" - githubEventCreate = "create" - githubEventDelete = "delete" - githubEventFork = "fork" - githubEventPush = "push" - githubEventIssues = "issues" - githubEventIssueComment = "issue_comment" - githubEventRelease = "release" - githubEventPullRequestComment = "pull_request_comment" - githubEventGollum = "gollum" + GithubEventPullRequest = "pull_request" + GithubEventPullRequestTarget = "pull_request_target" + GithubEventPullRequestReviewComment = "pull_request_review_comment" + GithubEventPullRequestReview = "pull_request_review" + GithubEventRegistryPackage = "registry_package" + GithubEventCreate = "create" + GithubEventDelete = "delete" + GithubEventFork = "fork" + GithubEventPush = "push" + GithubEventIssues = "issues" + GithubEventIssueComment = "issue_comment" + GithubEventRelease = "release" + GithubEventPullRequestComment = "pull_request_comment" + GithubEventGollum = "gollum" ) // canGithubEventMatch check if the input Github event can match any Gitea event. func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEventType) bool { switch eventName { - case githubEventRegistryPackage: + case GithubEventRegistryPackage: return triggedEvent == webhook_module.HookEventPackage // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum - case githubEventGollum: + case GithubEventGollum: return triggedEvent == webhook_module.HookEventWiki - case githubEventIssues: + case GithubEventIssues: switch triggedEvent { case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, @@ -46,7 +46,7 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent return false } - case githubEventPullRequest, githubEventPullRequestTarget: + case GithubEventPullRequest, GithubEventPullRequestTarget: switch triggedEvent { case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, @@ -58,7 +58,7 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent return false } - case githubEventPullRequestReview: + case GithubEventPullRequestReview: switch triggedEvent { case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewComment, diff --git a/modules/actions/github_test.go b/modules/actions/github_test.go index e7f4158ae2d1..4bf55ae03fcc 100644 --- a/modules/actions/github_test.go +++ b/modules/actions/github_test.go @@ -21,85 +21,85 @@ func TestCanGithubEventMatch(t *testing.T) { // registry_package event { "registry_package matches", - githubEventRegistryPackage, + GithubEventRegistryPackage, webhook_module.HookEventPackage, true, }, { "registry_package cannot match", - githubEventRegistryPackage, + GithubEventRegistryPackage, webhook_module.HookEventPush, false, }, // issues event { "issue matches", - githubEventIssues, + GithubEventIssues, webhook_module.HookEventIssueLabel, true, }, { "issue cannot match", - githubEventIssues, + GithubEventIssues, webhook_module.HookEventIssueComment, false, }, // issue_comment event { "issue_comment matches", - githubEventIssueComment, + GithubEventIssueComment, webhook_module.HookEventIssueComment, true, }, { "issue_comment cannot match", - githubEventIssueComment, + GithubEventIssueComment, webhook_module.HookEventIssues, false, }, // pull_request event { "pull_request matches", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequestSync, true, }, { "pull_request cannot match", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequestComment, false, }, // pull_request_target event { "pull_request_target matches", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequest, true, }, { "pull_request_target cannot match", - githubEventPullRequest, + GithubEventPullRequest, webhook_module.HookEventPullRequestComment, false, }, // pull_request_review event { "pull_request_review matches", - githubEventPullRequestReview, + GithubEventPullRequestReview, webhook_module.HookEventPullRequestReviewComment, true, }, { "pull_request_review cannot match", - githubEventPullRequestReview, + GithubEventPullRequestReview, webhook_module.HookEventPullRequestComment, false, }, // other events { "create event", - githubEventCreate, + GithubEventCreate, webhook_module.HookEventCreate, true, }, diff --git a/modules/actions/log.go b/modules/actions/log.go index 3868101992a9..cdf18646aaf0 100644 --- a/modules/actions/log.go +++ b/modules/actions/log.go @@ -29,12 +29,28 @@ const ( ) func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runnerv1.LogRow) ([]int, error) { + flag := os.O_WRONLY + if offset == 0 { + // Create file only if offset is 0, or it could result in content holes if the file doesn't exist. + flag |= os.O_CREATE + } name := DBFSPrefix + filename - f, err := dbfs.OpenFile(ctx, name, os.O_WRONLY|os.O_CREATE) + f, err := dbfs.OpenFile(ctx, name, flag) if err != nil { return nil, fmt.Errorf("dbfs OpenFile %q: %w", name, err) } defer f.Close() + + stat, err := f.Stat() + if err != nil { + return nil, fmt.Errorf("dbfs Stat %q: %w", name, err) + } + if stat.Size() < offset { + // If the size is less than offset, refuse to write, or it could result in content holes. + // However, if the size is greater than offset, we can still write to overwrite the content. + return nil, fmt.Errorf("size of %q is less than offset", name) + } + if _, err := f.Seek(offset, io.SeekStart); err != nil { return nil, fmt.Errorf("dbfs Seek %q: %w", name, err) } @@ -57,7 +73,7 @@ func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runne } func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) { - f, err := openLogs(ctx, inStorage, filename) + f, err := OpenLogs(ctx, inStorage, filename) if err != nil { return nil, err } @@ -125,7 +141,7 @@ func RemoveLogs(ctx context.Context, inStorage bool, filename string) error { return nil } -func openLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) { +func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) { if !inStorage { name := DBFSPrefix + filename f, err := dbfs.Open(ctx, name) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index d9459288b183..3786f2a274ce 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -20,6 +20,14 @@ import ( "gopkg.in/yaml.v3" ) +type DetectedWorkflow struct { + EntryName string + TriggerEvent string + Commit *git.Commit + Ref string + Content []byte +} + func init() { model.OnDecodeNodeError = func(node yaml.Node, out interface{}, err error) { // Log the error instead of panic or fatal. @@ -89,13 +97,13 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { return events, nil } -func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) (map[string][]byte, error) { +func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) ([]*DetectedWorkflow, error) { entries, err := ListWorkflows(commit) if err != nil { return nil, err } - workflows := make(map[string][]byte, len(entries)) + workflows := make([]*DetectedWorkflow, 0, len(entries)) for _, entry := range entries { content, err := GetContentFromEntry(entry) if err != nil { @@ -109,7 +117,13 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy for _, evt := range events { log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) if detectMatched(commit, triggedEvent, payload, evt) { - workflows[entry.Name()] = content + dwf := &DetectedWorkflow{ + EntryName: entry.Name(), + TriggerEvent: evt.Name, + Commit: commit, + Content: content, + } + workflows = append(workflows, dwf) } } } diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index 6ef5d5994229..2c374d2c0d0f 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -23,77 +23,77 @@ func TestDetectMatched(t *testing.T) { expected bool }{ { - desc: "HookEventCreate(create) matches githubEventCreate(create)", + desc: "HookEventCreate(create) matches GithubEventCreate(create)", triggedEvent: webhook_module.HookEventCreate, payload: nil, yamlOn: "on: create", expected: true, }, { - desc: "HookEventIssues(issues) `opened` action matches githubEventIssues(issues)", + desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)", triggedEvent: webhook_module.HookEventIssues, payload: &api.IssuePayload{Action: api.HookIssueOpened}, yamlOn: "on: issues", expected: true, }, { - desc: "HookEventIssues(issues) `milestoned` action matches githubEventIssues(issues)", + desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)", triggedEvent: webhook_module.HookEventIssues, payload: &api.IssuePayload{Action: api.HookIssueMilestoned}, yamlOn: "on: issues", expected: true, }, { - desc: "HookEventPullRequestSync(pull_request_sync) matches githubEventPullRequest(pull_request)", + desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)", triggedEvent: webhook_module.HookEventPullRequestSync, payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized}, yamlOn: "on: pull_request", expected: true, }, { - desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match githubEventPullRequest(pull_request) with no activity type", + desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type", triggedEvent: webhook_module.HookEventPullRequest, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, yamlOn: "on: pull_request", expected: false, }, { - desc: "HookEventPullRequest(pull_request) `label_updated` action matches githubEventPullRequest(pull_request) with `label` activity type", + desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type", triggedEvent: webhook_module.HookEventPullRequest, payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated}, yamlOn: "on:\n pull_request:\n types: [labeled]", expected: true, }, { - desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches githubEventPullRequestReviewComment(pull_request_review_comment)", + desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)", triggedEvent: webhook_module.HookEventPullRequestReviewComment, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, yamlOn: "on:\n pull_request_review_comment:\n types: [created]", expected: true, }, { - desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match githubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)", + desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)", triggedEvent: webhook_module.HookEventPullRequestReviewRejected, payload: &api.PullRequestPayload{Action: api.HookIssueReviewed}, yamlOn: "on:\n pull_request_review:\n types: [dismissed]", expected: false, }, { - desc: "HookEventRelease(release) `published` action matches githubEventRelease(release) with `published` activity type", + desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type", triggedEvent: webhook_module.HookEventRelease, payload: &api.ReleasePayload{Action: api.HookReleasePublished}, yamlOn: "on:\n release:\n types: [published]", expected: true, }, { - desc: "HookEventPackage(package) `created` action doesn't match githubEventRegistryPackage(registry_package) with `updated` activity type", + desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type", triggedEvent: webhook_module.HookEventPackage, payload: &api.PackagePayload{Action: api.HookPackageCreated}, yamlOn: "on:\n registry_package:\n types: [updated]", expected: false, }, { - desc: "HookEventWiki(wiki) matches githubEventGollum(gollum)", + desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)", triggedEvent: webhook_module.HookEventWiki, payload: nil, yamlOn: "on: gollum", diff --git a/modules/base/tool.go b/modules/base/tool.go index 13b07c043e47..004781835a59 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -210,7 +210,7 @@ func EntryIcon(entry *git.TreeEntry) string { return "file-symlink-file" } if te.IsDir() { - return "file-submodule" + return "file-directory-symlink" } return "file-symlink-file" case entry.IsDir(): diff --git a/modules/context/repo.go b/modules/context/repo.go index fd5f20857663..e99908525128 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -593,7 +593,7 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["RepoSearchEnabled"] = setting.Indexer.RepoIndexerEnabled if setting.Indexer.RepoIndexerEnabled { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } if ctx.IsSigned { @@ -667,13 +667,38 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { } ctx.Data["Tags"] = tags - brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) + branchOpts := git_model.FindBranchOptions{ + RepoID: ctx.Repo.Repository.ID, + IsDeletedBranch: util.OptionalBoolFalse, + ListOptions: db.ListOptions{ + ListAll: true, + }, + } + branchesTotal, err := git_model.CountBranches(ctx, branchOpts) + if err != nil { + ctx.ServerError("CountBranches", err) + return + } + + // non empty repo should have at least 1 branch, so this repository's branches haven't been synced yet + if branchesTotal == 0 { // fallback to do a sync immediately + branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) + if err != nil { + ctx.ServerError("SyncRepoBranches", err) + return + } + } + + // FIXME: use paganation and async loading + branchOpts.ExcludeBranchNames = []string{ctx.Repo.Repository.DefaultBranch} + brs, err := git_model.FindBranchNames(ctx, branchOpts) if err != nil { ctx.ServerError("GetBranches", err) return } - ctx.Data["Branches"] = brs - ctx.Data["BranchesCount"] = len(brs) + // always put default branch on the top + ctx.Data["Branches"] = append(branchOpts.ExcludeBranchNames, brs...) + ctx.Data["BranchesCount"] = branchesTotal // If not branch selected, try default one. // If default branch doesn't exist, fall back to some other branch. @@ -897,9 +922,9 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context if len(ctx.Params("*")) == 0 { refName = ctx.Repo.Repository.DefaultBranch if !ctx.Repo.GitRepo.IsBranchExist(refName) { - brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) + brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1) if err == nil && len(brs) != 0 { - refName = brs[0] + refName = brs[0].Name } else if len(brs) == 0 { log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) ctx.Repo.Repository.MarkAsBrokenEmpty() diff --git a/modules/emoji/emoji_data.go b/modules/emoji/emoji_data.go index a365e423cbe5..8d0ae0a43e67 100644 --- a/modules/emoji/emoji_data.go +++ b/modules/emoji/emoji_data.go @@ -230,6 +230,7 @@ var GemojiData = Gemoji{ {"\U0001f382", "birthday cake", []string{"birthday"}, "6.0", false}, {"\U0001f9ac", "bison", []string{"bison"}, "13.0", false}, {"\U0001fae6", "biting lip", []string{"biting_lip"}, "14.0", false}, + {"\U0001f426\u200d\u2b1b", "black bird", []string{"black_bird"}, "15.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}, @@ -748,6 +749,7 @@ var GemojiData = Gemoji{ {"\U0001f42c", "dolphin", []string{"dolphin", "flipper"}, "6.0", false}, {"\U0001f1e9\U0001f1f2", "flag: Dominica", []string{"dominica"}, "6.0", false}, {"\U0001f1e9\U0001f1f4", "flag: Dominican Republic", []string{"dominican_republic"}, "6.0", false}, + {"\U0001facf", "donkey", []string{"donkey"}, "15.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}, @@ -982,11 +984,13 @@ var GemojiData = Gemoji{ {"\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}, + {"\U0001fa88", "flute", []string{"flute"}, "15.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}, + {"\U0001faad", "folding hand fan", []string{"folding_hand_fan"}, "15.0", false}, {"\U0001fad5", "fondue", []string{"fondue"}, "13.0", false}, {"\U0001f9b6", "foot", []string{"foot"}, "11.0", true}, {"\U0001f9b6\U0001f3ff", "foot: Dark Skin Tone", []string{"foot_Dark_Skin_Tone"}, "12.0", false}, @@ -1054,6 +1058,7 @@ var GemojiData = Gemoji{ {"\U0001f1ec\U0001f1ee", "flag: Gibraltar", []string{"gibraltar"}, "6.0", false}, {"\U0001f381", "wrapped gift", []string{"gift"}, "6.0", false}, {"\U0001f49d", "heart with ribbon", []string{"gift_heart"}, "6.0", false}, + {"\U0001fada", "ginger root", []string{"ginger_root"}, "15.0", false}, {"\U0001f992", "giraffe", []string{"giraffe"}, "11.0", false}, {"\U0001f467", "girl", []string{"girl"}, "6.0", true}, {"\U0001f467\U0001f3ff", "girl: Dark Skin Tone", []string{"girl_Dark_Skin_Tone"}, "12.0", false}, @@ -1085,6 +1090,7 @@ var GemojiData = Gemoji{ {"\U0001f3cc\U0001f3fe\ufe0f\u200d\u2640\ufe0f", "woman golfing: Medium-Dark Skin Tone", []string{"golfing_woman_Medium-Dark_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}, + {"\U0001fabf", "goose", []string{"goose"}, "15.0", false}, {"\U0001f98d", "gorilla", []string{"gorilla"}, "9.0", false}, {"\U0001f347", "grapes", []string{"grapes"}, "6.0", false}, {"\U0001f1ec\U0001f1f7", "flag: Greece", []string{"greece"}, "6.0", false}, @@ -1097,6 +1103,7 @@ var GemojiData = Gemoji{ {"\U0001f1ec\U0001f1f1", "flag: Greenland", []string{"greenland"}, "6.0", false}, {"\U0001f1ec\U0001f1e9", "flag: Grenada", []string{"grenada"}, "6.0", false}, {"\u2755", "white exclamation mark", []string{"grey_exclamation"}, "6.0", false}, + {"\U0001fa76", "grey heart", []string{"grey_heart"}, "15.0", false}, {"\u2754", "white question mark", []string{"grey_question"}, "6.0", false}, {"\U0001f62c", "grimacing face", []string{"grimacing"}, "6.1", false}, {"\U0001f601", "beaming face with smiling eyes", []string{"grin"}, "6.0", false}, @@ -1129,6 +1136,7 @@ var GemojiData = Gemoji{ {"\U0001f3b8", "guitar", []string{"guitar"}, "6.0", false}, {"\U0001f52b", "water pistol", []string{"gun"}, "6.0", false}, {"\U0001f1ec\U0001f1fe", "flag: Guyana", []string{"guyana"}, "6.0", false}, + {"\U0001faae", "hair pick", []string{"hair_pick"}, "15.0", false}, {"\U0001f487", "person getting haircut", []string{"haircut"}, "6.0", true}, {"\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}, @@ -1253,6 +1261,7 @@ var GemojiData = Gemoji{ {"\U0001f1ed\U0001f1fa", "flag: Hungary", []string{"hungary"}, "6.0", false}, {"\U0001f62f", "hushed face", []string{"hushed"}, "6.1", false}, {"\U0001f6d6", "hut", []string{"hut"}, "13.0", false}, + {"\U0001fabb", "hyacinth", []string{"hyacinth"}, "15.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}, @@ -1293,6 +1302,7 @@ var GemojiData = Gemoji{ {"\U0001f479", "ogre", []string{"japanese_ogre"}, "6.0", false}, {"\U0001fad9", "jar", []string{"jar"}, "14.0", false}, {"\U0001f456", "jeans", []string{"jeans"}, "6.0", false}, + {"\U0001fabc", "jellyfish", []string{"jellyfish"}, "15.0", false}, {"\U0001f1ef\U0001f1ea", "flag: Jersey", []string{"jersey"}, "6.0", false}, {"\U0001f9e9", "puzzle piece", []string{"jigsaw"}, "11.0", false}, {"\U0001f1ef\U0001f1f4", "flag: Jordan", []string{"jordan"}, "6.0", false}, @@ -1319,6 +1329,7 @@ var GemojiData = Gemoji{ {"\U0001f511", "key", []string{"key"}, "6.0", false}, {"\u2328\ufe0f", "keyboard", []string{"keyboard"}, "", false}, {"\U0001f51f", "keycap: 10", []string{"keycap_ten"}, "6.0", false}, + {"\U0001faaf", "khanda", []string{"khanda"}, "15.0", false}, {"\U0001f6f4", "kick scooter", []string{"kick_scooter"}, "9.0", false}, {"\U0001f458", "kimono", []string{"kimono"}, "6.0", false}, {"\U0001f1f0\U0001f1ee", "flag: Kiribati", []string{"kiribati"}, "6.0", false}, @@ -1383,6 +1394,12 @@ var GemojiData = Gemoji{ {"\U0001faf2\U0001f3fe", "leftwards hand: Medium-Dark Skin Tone", []string{"leftwards_hand_Medium-Dark_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}, + {"\U0001faf7", "leftwards pushing hand", []string{"leftwards_pushing_hand"}, "15.0", true}, + {"\U0001faf7\U0001f3ff", "leftwards pushing hand: Dark Skin Tone", []string{"leftwards_pushing_hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf7\U0001f3fb", "leftwards pushing hand: Light Skin Tone", []string{"leftwards_pushing_hand_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf7\U0001f3fe", "leftwards pushing hand: Medium-Dark Skin Tone", []string{"leftwards_pushing_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf7\U0001f3fc", "leftwards pushing hand: Medium-Light Skin Tone", []string{"leftwards_pushing_hand_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf7\U0001f3fd", "leftwards pushing hand: Medium Skin Tone", []string{"leftwards_pushing_hand_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9b5", "leg", []string{"leg"}, "11.0", true}, {"\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}, @@ -1398,6 +1415,7 @@ var GemojiData = Gemoji{ {"\u264e", "Libra", []string{"libra"}, "", false}, {"\U0001f1f1\U0001f1fe", "flag: Libya", []string{"libya"}, "6.0", false}, {"\U0001f1f1\U0001f1ee", "flag: Liechtenstein", []string{"liechtenstein"}, "6.0", false}, + {"\U0001fa75", "light blue heart", []string{"light_blue_heart"}, "15.0", false}, {"\U0001f688", "light rail", []string{"light_rail"}, "6.0", false}, {"\U0001f517", "link", []string{"link"}, "6.0", false}, {"\U0001f981", "lion", []string{"lion"}, "8.0", false}, @@ -1695,6 +1713,7 @@ var GemojiData = Gemoji{ {"\U0001f570\ufe0f", "mantelpiece clock", []string{"mantelpiece_clock"}, "7.0", false}, {"\U0001f9bd", "manual wheelchair", []string{"manual_wheelchair"}, "12.0", false}, {"\U0001f341", "maple leaf", []string{"maple_leaf"}, "6.0", false}, + {"\U0001fa87", "maracas", []string{"maracas"}, "15.0", false}, {"\U0001f1f2\U0001f1ed", "flag: Marshall Islands", []string{"marshall_islands"}, "6.0", false}, {"\U0001f94b", "martial arts uniform", []string{"martial_arts_uniform"}, "9.0", false}, {"\U0001f1f2\U0001f1f6", "flag: Martinique", []string{"martinique"}, "6.0", false}, @@ -1799,6 +1818,7 @@ var GemojiData = Gemoji{ {"\U0001f1f2\U0001f1f8", "flag: Montserrat", []string{"montserrat"}, "6.0", false}, {"\U0001f314", "waxing gibbous moon", []string{"moon", "waxing_gibbous_moon"}, "6.0", false}, {"\U0001f96e", "moon cake", []string{"moon_cake"}, "11.0", false}, + {"\U0001face", "moose", []string{"moose"}, "15.0", false}, {"\U0001f1f2\U0001f1e6", "flag: Morocco", []string{"morocco"}, "6.0", false}, {"\U0001f393", "graduation cap", []string{"mortar_board"}, "6.0", false}, {"\U0001f54c", "mosque", []string{"mosque"}, "8.0", false}, @@ -2076,6 +2096,7 @@ var GemojiData = Gemoji{ {"\U0001f6f3\ufe0f", "passenger ship", []string{"passenger_ship"}, "7.0", false}, {"\U0001f6c2", "passport control", []string{"passport_control"}, "6.0", false}, {"\u23f8\ufe0f", "pause button", []string{"pause_button"}, "7.0", false}, + {"\U0001fadb", "pea pod", []string{"pea_pod"}, "15.0", false}, {"\u262e\ufe0f", "peace symbol", []string{"peace_symbol"}, "", false}, {"\U0001f351", "peach", []string{"peach"}, "6.0", false}, {"\U0001f99a", "peacock", []string{"peacock"}, "11.0", false}, @@ -2085,7 +2106,12 @@ 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", false}, + {"\U0001f9d1\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands", []string{"people_holding_hands"}, "12.0", true}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands: Dark Skin Tone", []string{"people_holding_hands_Dark_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\U0001f3fe\u200d\U0001f91d\u200d\U0001f9d1", "people holding hands: Medium-Dark Skin Tone", []string{"people_holding_hands_Medium-Dark_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}, {"\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}, @@ -2194,6 +2220,7 @@ var GemojiData = Gemoji{ {"\U0001f90f\U0001f3fd", "pinching hand: Medium Skin Tone", []string{"pinching_hand_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f34d", "pineapple", []string{"pineapple"}, "6.0", false}, {"\U0001f3d3", "ping pong", []string{"ping_pong"}, "8.0", false}, + {"\U0001fa77", "pink heart", []string{"pink_heart"}, "15.0", false}, {"\U0001f3f4\u200d\u2620\ufe0f", "pirate flag", []string{"pirate_flag"}, "11.0", false}, {"\u2653", "Pisces", []string{"pisces"}, "", false}, {"\U0001f1f5\U0001f1f3", "flag: Pitcairn Islands", []string{"pitcairn_islands"}, "6.0", false}, @@ -2346,7 +2373,7 @@ var GemojiData = Gemoji{ {"\U0001f4fb", "radio", []string{"radio"}, "6.0", false}, {"\U0001f518", "radio button", []string{"radio_button"}, "6.0", false}, {"\u2622\ufe0f", "radioactive", []string{"radioactive"}, "", false}, - {"\U0001f621", "pouting face", []string{"rage", "pout"}, "6.0", false}, + {"\U0001f621", "enraged face", []string{"rage", "pout"}, "6.0", false}, {"\U0001f683", "railway car", []string{"railway_car"}, "6.0", false}, {"\U0001f6e4\ufe0f", "railway track", []string{"railway_track"}, "7.0", false}, {"\U0001f308", "rainbow", []string{"rainbow"}, "6.0", false}, @@ -2434,6 +2461,12 @@ var GemojiData = Gemoji{ {"\U0001faf1\U0001f3fe", "rightwards hand: Medium-Dark Skin Tone", []string{"rightwards_hand_Medium-Dark_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}, + {"\U0001faf8", "rightwards pushing hand", []string{"rightwards_pushing_hand"}, "15.0", true}, + {"\U0001faf8\U0001f3ff", "rightwards pushing hand: Dark Skin Tone", []string{"rightwards_pushing_hand_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf8\U0001f3fb", "rightwards pushing hand: Light Skin Tone", []string{"rightwards_pushing_hand_Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf8\U0001f3fe", "rightwards pushing hand: Medium-Dark Skin Tone", []string{"rightwards_pushing_hand_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001faf8\U0001f3fc", "rightwards pushing hand: Medium-Light Skin Tone", []string{"rightwards_pushing_hand_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001faf8\U0001f3fd", "rightwards pushing hand: Medium Skin Tone", []string{"rightwards_pushing_hand_Medium_Skin_Tone"}, "12.0", false}, {"\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}, @@ -2566,6 +2599,7 @@ var GemojiData = Gemoji{ {"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}, + {"\U0001fae8", "shaking face", []string{"shaking_face"}, "15.0", false}, {"\U0001f958", "shallow pan of food", []string{"shallow_pan_of_food"}, "", false}, {"\u2618\ufe0f", "shamrock", []string{"shamrock"}, "4.1", false}, {"\U0001f988", "shark", []string{"shark"}, "9.0", false}, @@ -3125,7 +3159,9 @@ var GemojiData = Gemoji{ {"\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}, + {"\U0001fabd", "wing", []string{"wing"}, "15.0", false}, {"\U0001f609", "winking face", []string{"wink"}, "6.0", false}, + {"\U0001f6dc", "wireless", []string{"wireless"}, "15.0", false}, {"\U0001f43a", "wolf", []string{"wolf"}, "6.0", false}, {"\U0001f469", "woman", []string{"woman"}, "6.0", true}, {"\U0001f469\U0001f3ff", "woman: Dark Skin Tone", []string{"woman_Dark_Skin_Tone"}, "12.0", false}, @@ -3364,5 +3400,5 @@ var GemojiData = Gemoji{ {"\U0001f9df", "zombie", []string{"zombie"}, "11.0", false}, {"\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}, + {"\U0001f4a4", "ZZZ", []string{"zzz"}, "6.0", false}, } diff --git a/modules/indexer/code/bleve.go b/modules/indexer/code/bleve/bleve.go similarity index 69% rename from modules/indexer/code/bleve.go rename to modules/indexer/code/bleve/bleve.go index 5936613e3ad8..33cc4e02b514 100644 --- a/modules/indexer/code/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -1,14 +1,13 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package code +package bleve import ( "bufio" "context" "fmt" "io" - "os" "strconv" "strings" "time" @@ -17,12 +16,13 @@ import ( "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - gitea_bleve "code.gitea.io/gitea/modules/indexer/bleve" + "code.gitea.io/gitea/modules/indexer/code/internal" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/typesniffer" - "code.gitea.io/gitea/modules/util" "github.com/blevesearch/bleve/v2" analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" @@ -31,10 +31,8 @@ import ( "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" - "github.com/blevesearch/bleve/v2/index/upsidedown" "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search/query" - "github.com/ethantkoenig/rupture" "github.com/go-enry/go-enry/v2" ) @@ -59,38 +57,6 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { }) } -// openBleveIndexer open the index at the specified path, checking for metadata -// updates and bleve version updates. If index needs to be created (or -// re-created), returns (nil, nil) -func openBleveIndexer(path string, latestVersion int) (bleve.Index, error) { - _, err := os.Stat(path) - if err != nil && os.IsNotExist(err) { - return nil, nil - } else if err != nil { - return nil, err - } - - metadata, err := rupture.ReadIndexMetadata(path) - if err != nil { - return nil, err - } - if metadata.Version < latestVersion { - // the indexer is using a previous version, so we should delete it and - // re-populate - return nil, util.RemoveAll(path) - } - - index, err := bleve.Open(path) - if err != nil && err == upsidedown.IncompatibleVersion { - // the indexer was built with a previous version of bleve, so we should - // delete it and re-populate - return nil, util.RemoveAll(path) - } else if err != nil { - return nil, err - } - return index, nil -} - // RepoIndexerData data stored in the repo indexer type RepoIndexerData struct { RepoID int64 @@ -111,8 +77,8 @@ const ( repoIndexerLatestVersion = 6 ) -// createBleveIndexer create a bleve repo indexer if one does not already exist -func createBleveIndexer(path string, latestVersion int) (bleve.Index, error) { +// generateBleveIndexMapping generates a bleve index mapping for the repo indexer +func generateBleveIndexMapping() (mapping.IndexMapping, error) { docMapping := bleve.NewDocumentMapping() numericFieldMapping := bleve.NewNumericFieldMapping() numericFieldMapping.IncludeInAll = false @@ -147,42 +113,28 @@ func createBleveIndexer(path string, latestVersion int) (bleve.Index, error) { mapping.AddDocumentMapping(repoIndexerDocType, docMapping) mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping()) - indexer, err := bleve.New(path, mapping) - if err != nil { - return nil, err - } - - if err = rupture.WriteIndexMetadata(path, &rupture.IndexMetadata{ - Version: latestVersion, - }); err != nil { - return nil, err - } - return indexer, nil + return mapping, nil } -var _ Indexer = &BleveIndexer{} +var _ internal.Indexer = &Indexer{} -// BleveIndexer represents a bleve indexer implementation -type BleveIndexer struct { - indexDir string - indexer bleve.Index +// Indexer represents a bleve indexer implementation +type Indexer struct { + inner *inner_bleve.Indexer + indexer_internal.Indexer // do not composite inner_bleve.Indexer directly to avoid exposing too much } -// NewBleveIndexer creates a new bleve local indexer -func NewBleveIndexer(indexDir string) (*BleveIndexer, bool, error) { - indexer := &BleveIndexer{ - indexDir: indexDir, +// NewIndexer creates a new bleve local indexer +func NewIndexer(indexDir string) *Indexer { + inner := inner_bleve.NewIndexer(indexDir, repoIndexerLatestVersion, generateBleveIndexMapping) + return &Indexer{ + Indexer: inner, + inner: inner, } - created, err := indexer.init() - if err != nil { - indexer.Close() - return nil, false, err - } - return indexer, created, err } -func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, commitSha string, - update fileUpdate, repo *repo_model.Repository, batch *gitea_bleve.FlushingBatch, +func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, commitSha string, + update internal.FileUpdate, repo *repo_model.Repository, batch *inner_bleve.FlushingBatch, ) error { // Ignore vendored files in code search if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) { @@ -227,7 +179,7 @@ func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteClose if _, err = batchReader.Discard(1); err != nil { return err } - id := filenameIndexerID(repo.ID, update.Filename) + id := internal.FilenameIndexerID(repo.ID, update.Filename) return batch.Index(id, &RepoIndexerData{ RepoID: repo.ID, CommitID: commitSha, @@ -237,50 +189,14 @@ func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteClose }) } -func (b *BleveIndexer) addDelete(filename string, repo *repo_model.Repository, batch *gitea_bleve.FlushingBatch) error { - id := filenameIndexerID(repo.ID, filename) +func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch *inner_bleve.FlushingBatch) error { + id := internal.FilenameIndexerID(repo.ID, filename) return batch.Delete(id) } -// init init the indexer -func (b *BleveIndexer) init() (bool, error) { - var err error - b.indexer, err = openBleveIndexer(b.indexDir, repoIndexerLatestVersion) - if err != nil { - return false, err - } - if b.indexer != nil { - return false, nil - } - - b.indexer, err = createBleveIndexer(b.indexDir, repoIndexerLatestVersion) - if err != nil { - return false, err - } - - return true, nil -} - -// Close close the indexer -func (b *BleveIndexer) Close() { - log.Debug("Closing repo indexer") - if b.indexer != nil { - err := b.indexer.Close() - if err != nil { - log.Error("Error whilst closing the repository indexer: %v", err) - } - } - log.Info("PID: %d Repository Indexer closed", os.Getpid()) -} - -// Ping does nothing -func (b *BleveIndexer) Ping() bool { - return true -} - // Index indexes the data -func (b *BleveIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error { - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) +func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) if len(changes.Updates) > 0 { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! @@ -308,14 +224,14 @@ func (b *BleveIndexer) Index(ctx context.Context, repo *repo_model.Repository, s } // Delete deletes indexes by ids -func (b *BleveIndexer) Delete(repoID int64) error { +func (b *Indexer) Delete(_ context.Context, repoID int64) error { query := numericEqualityQuery(repoID, "RepoID") searchRequest := bleve.NewSearchRequestOptions(query, 2147483647, 0, false) - result, err := b.indexer.Search(searchRequest) + result, err := b.inner.Indexer.Search(searchRequest) if err != nil { return err } - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) for _, hit := range result.Hits { if err = batch.Delete(hit.ID); err != nil { return err @@ -326,7 +242,7 @@ func (b *BleveIndexer) Delete(repoID int64) error { // Search searches for files in the specified repo. // Returns the matching file-paths -func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { +func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { var ( indexerQuery query.Query keywordQuery query.Query @@ -379,14 +295,14 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10)) } - result, err := b.indexer.SearchInContext(ctx, searchRequest) + result, err := b.inner.Indexer.SearchInContext(ctx, searchRequest) if err != nil { return 0, nil, nil, err } total := int64(result.Total) - searchResults := make([]*SearchResult, len(result.Hits)) + searchResults := make([]*internal.SearchResult, len(result.Hits)) for i, hit := range result.Hits { startIndex, endIndex := -1, -1 for _, locations := range hit.Locations["Content"] { @@ -405,11 +321,11 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke if t, err := time.Parse(time.RFC3339, hit.Fields["UpdatedAt"].(string)); err == nil { updatedUnix = timeutil.TimeStamp(t.Unix()) } - searchResults[i] = &SearchResult{ + searchResults[i] = &internal.SearchResult{ RepoID: int64(hit.Fields["RepoID"].(float64)), StartIndex: startIndex, EndIndex: endIndex, - Filename: filenameOfIndexerID(hit.ID), + Filename: internal.FilenameOfIndexerID(hit.ID), Content: hit.Fields["Content"].(string), CommitID: hit.Fields["CommitID"].(string), UpdatedUnix: updatedUnix, @@ -418,7 +334,7 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke } } - searchResultLanguages := make([]*SearchResultLanguages, 0, 10) + searchResultLanguages := make([]*internal.SearchResultLanguages, 0, 10) if len(language) > 0 { // Use separate query to go get all language counts facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false) @@ -426,7 +342,7 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke facetRequest.IncludeLocations = true facetRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10)) - if result, err = b.indexer.Search(facetRequest); err != nil { + if result, err = b.inner.Indexer.Search(facetRequest); err != nil { return 0, nil, nil, err } @@ -436,7 +352,7 @@ func (b *BleveIndexer) Search(ctx context.Context, repoIDs []int64, language, ke if len(term.Term) == 0 { continue } - searchResultLanguages = append(searchResultLanguages, &SearchResultLanguages{ + searchResultLanguages = append(searchResultLanguages, &internal.SearchResultLanguages{ Language: term.Term, Color: enry.GetColor(term.Term), Count: term.Count, diff --git a/modules/indexer/code/bleve_test.go b/modules/indexer/code/bleve_test.go deleted file mode 100644 index 00bcd5c90c67..000000000000 --- a/modules/indexer/code/bleve_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package code - -import ( - "testing" - - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestBleveIndexAndSearch(t *testing.T) { - unittest.PrepareTestEnv(t) - - dir := t.TempDir() - - idx, _, err := NewBleveIndexer(dir) - if err != nil { - assert.Fail(t, "Unable to create bleve indexer Error: %v", err) - if idx != nil { - idx.Close() - } - return - } - defer idx.Close() - - testIndexer("beleve", t, idx) -} diff --git a/modules/indexer/code/elastic_search_test.go b/modules/indexer/code/elastic_search_test.go deleted file mode 100644 index e7506eefa680..000000000000 --- a/modules/indexer/code/elastic_search_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package code - -import ( - "os" - "testing" - - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestESIndexAndSearch(t *testing.T) { - unittest.PrepareTestEnv(t) - - u := os.Getenv("TEST_INDEXER_CODE_ES_URL") - if u == "" { - t.SkipNow() - return - } - - indexer, _, err := NewElasticSearchIndexer(u, "gitea_codes") - if err != nil { - assert.Fail(t, "Unable to create ES indexer Error: %v", err) - if indexer != nil { - indexer.Close() - } - return - } - defer indexer.Close() - - testIndexer("elastic_search", t, indexer) -} - -func TestIndexPos(t *testing.T) { - startIdx, endIdx := indexPos("test index start and end", "start", "end") - assert.EqualValues(t, 11, startIdx) - assert.EqualValues(t, 24, endIdx) -} diff --git a/modules/indexer/code/elastic_search.go b/modules/indexer/code/elasticsearch/elasticsearch.go similarity index 56% rename from modules/indexer/code/elastic_search.go rename to modules/indexer/code/elasticsearch/elasticsearch.go index 0e56a865880e..88054585cd28 100644 --- a/modules/indexer/code/elastic_search.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -1,25 +1,23 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package code +package elasticsearch import ( "bufio" "context" - "errors" "fmt" "io" - "net" "strconv" "strings" - "sync" - "time" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/indexer/code/internal" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -38,63 +36,22 @@ const ( esMultiMatchTypePhrasePrefix = "phrase_prefix" ) -var _ Indexer = &ElasticSearchIndexer{} +var _ internal.Indexer = &Indexer{} -// ElasticSearchIndexer implements Indexer interface -type ElasticSearchIndexer struct { - client *elastic.Client - indexerAliasName string - available bool - stopTimer chan struct{} - lock sync.RWMutex +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_elasticsearch.Indexer + indexer_internal.Indexer // do not composite inner_elasticsearch.Indexer directly to avoid exposing too much } -// NewElasticSearchIndexer creates a new elasticsearch indexer -func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, bool, error) { - opts := []elastic.ClientOptionFunc{ - elastic.SetURL(url), - elastic.SetSniff(false), - elastic.SetHealthcheckInterval(10 * time.Second), - elastic.SetGzip(false), +// NewIndexer creates a new elasticsearch indexer +func NewIndexer(url, indexerName string) *Indexer { + inner := inner_elasticsearch.NewIndexer(url, indexerName, esRepoIndexerLatestVersion, defaultMapping) + indexer := &Indexer{ + inner: inner, + Indexer: inner, } - - logger := log.GetLogger(log.DEFAULT) - - opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace})) - opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info})) - opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error})) - - client, err := elastic.NewClient(opts...) - if err != nil { - return nil, false, err - } - - indexer := &ElasticSearchIndexer{ - client: client, - indexerAliasName: indexerName, - available: true, - stopTimer: make(chan struct{}), - } - - ticker := time.NewTicker(10 * time.Second) - go func() { - for { - select { - case <-ticker.C: - indexer.checkAvailability() - case <-indexer.stopTimer: - ticker.Stop() - return - } - } - }() - - exists, err := indexer.init() - if err != nil { - indexer.Close() - return nil, false, err - } - return indexer, !exists, err + return indexer } const ( @@ -127,72 +84,7 @@ const ( }` ) -func (b *ElasticSearchIndexer) realIndexerName() string { - return fmt.Sprintf("%s.v%d", b.indexerAliasName, esRepoIndexerLatestVersion) -} - -// Init will initialize the indexer -func (b *ElasticSearchIndexer) init() (bool, error) { - ctx := graceful.GetManager().HammerContext() - exists, err := b.client.IndexExists(b.realIndexerName()).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !exists { - mapping := defaultMapping - - createIndex, err := b.client.CreateIndex(b.realIndexerName()).BodyString(mapping).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !createIndex.Acknowledged { - return false, fmt.Errorf("create index %s with %s failed", b.realIndexerName(), mapping) - } - } - - // check version - r, err := b.client.Aliases().Do(ctx) - if err != nil { - return false, b.checkError(err) - } - - realIndexerNames := r.IndicesByAlias(b.indexerAliasName) - if len(realIndexerNames) < 1 { - res, err := b.client.Alias(). - Add(b.realIndexerName(), b.indexerAliasName). - Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !res.Acknowledged { - return false, fmt.Errorf("create alias %s to index %s failed", b.indexerAliasName, b.realIndexerName()) - } - } else if len(realIndexerNames) >= 1 && realIndexerNames[0] < b.realIndexerName() { - log.Warn("Found older gitea indexer named %s, but we will create a new one %s and keep the old NOT DELETED. You can delete the old version after the upgrade succeed.", - realIndexerNames[0], b.realIndexerName()) - res, err := b.client.Alias(). - Remove(realIndexerNames[0], b.indexerAliasName). - Add(b.realIndexerName(), b.indexerAliasName). - Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !res.Acknowledged { - return false, fmt.Errorf("change alias %s to index %s failed", b.indexerAliasName, b.realIndexerName()) - } - } - - return exists, nil -} - -// Ping checks if elastic is available -func (b *ElasticSearchIndexer) Ping() bool { - b.lock.RLock() - defer b.lock.RUnlock() - return b.available -} - -func (b *ElasticSearchIndexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, sha string, update fileUpdate, repo *repo_model.Repository) ([]elastic.BulkableRequest, error) { +func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, sha string, update internal.FileUpdate, repo *repo_model.Repository) ([]elastic.BulkableRequest, error) { // Ignore vendored files in code search if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) { return nil, nil @@ -235,11 +127,11 @@ func (b *ElasticSearchIndexer) addUpdate(ctx context.Context, batchWriter git.Wr if _, err = batchReader.Discard(1); err != nil { return nil, err } - id := filenameIndexerID(repo.ID, update.Filename) + id := internal.FilenameIndexerID(repo.ID, update.Filename) return []elastic.BulkableRequest{ elastic.NewBulkIndexRequest(). - Index(b.indexerAliasName). + Index(b.inner.VersionedIndexName()). Id(id). Doc(map[string]interface{}{ "repo_id": repo.ID, @@ -251,15 +143,15 @@ func (b *ElasticSearchIndexer) addUpdate(ctx context.Context, batchWriter git.Wr }, nil } -func (b *ElasticSearchIndexer) addDelete(filename string, repo *repo_model.Repository) elastic.BulkableRequest { - id := filenameIndexerID(repo.ID, filename) +func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elastic.BulkableRequest { + id := internal.FilenameIndexerID(repo.ID, filename) return elastic.NewBulkDeleteRequest(). - Index(b.indexerAliasName). + Index(b.inner.VersionedIndexName()). Id(id) } // Index will save the index data -func (b *ElasticSearchIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error { +func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { reqs := make([]elastic.BulkableRequest, 0) if len(changes.Updates) > 0 { // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! @@ -288,21 +180,21 @@ func (b *ElasticSearchIndexer) Index(ctx context.Context, repo *repo_model.Repos } if len(reqs) > 0 { - _, err := b.client.Bulk(). - Index(b.indexerAliasName). + _, err := b.inner.Client.Bulk(). + Index(b.inner.VersionedIndexName()). Add(reqs...). Do(ctx) - return b.checkError(err) + return err } return nil } // Delete deletes indexes by ids -func (b *ElasticSearchIndexer) Delete(repoID int64) error { - _, err := b.client.DeleteByQuery(b.indexerAliasName). +func (b *Indexer) Delete(ctx context.Context, repoID int64) error { + _, err := b.inner.Client.DeleteByQuery(b.inner.VersionedIndexName()). Query(elastic.NewTermsQuery("repo_id", repoID)). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) + Do(ctx) + return err } // indexPos find words positions for start and the following end on content. It will @@ -321,8 +213,8 @@ func indexPos(content, start, end string) (int, int) { return startIdx, startIdx + len(start) + endIdx + len(end) } -func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) (int64, []*SearchResult, []*SearchResultLanguages, error) { - hits := make([]*SearchResult, 0, pageSize) +func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { + hits := make([]*internal.SearchResult, 0, pageSize) for _, hit := range searchResult.Hits.Hits { // FIXME: There is no way to get the position the keyword on the content currently on the same request. // So we get it from content, this may made the query slower. See @@ -341,7 +233,7 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) panic(fmt.Sprintf("2===%#v", hit.Highlight)) } - repoID, fileName := parseIndexerID(hit.Id) + repoID, fileName := internal.ParseIndexerID(hit.Id) res := make(map[string]interface{}) if err := json.Unmarshal(hit.Source, &res); err != nil { return 0, nil, nil, err @@ -349,7 +241,7 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) language := res["language"].(string) - hits = append(hits, &SearchResult{ + hits = append(hits, &internal.SearchResult{ RepoID: repoID, Filename: fileName, CommitID: res["commit_id"].(string), @@ -365,14 +257,14 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) return searchResult.TotalHits(), hits, extractAggs(searchResult), nil } -func extractAggs(searchResult *elastic.SearchResult) []*SearchResultLanguages { - var searchResultLanguages []*SearchResultLanguages +func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLanguages { + var searchResultLanguages []*internal.SearchResultLanguages agg, found := searchResult.Aggregations.Terms("language") if found { - searchResultLanguages = make([]*SearchResultLanguages, 0, 10) + searchResultLanguages = make([]*internal.SearchResultLanguages, 0, 10) for _, bucket := range agg.Buckets { - searchResultLanguages = append(searchResultLanguages, &SearchResultLanguages{ + searchResultLanguages = append(searchResultLanguages, &internal.SearchResultLanguages{ Language: bucket.Key.(string), Color: enry.GetColor(bucket.Key.(string)), Count: int(bucket.DocCount), @@ -383,7 +275,7 @@ func extractAggs(searchResult *elastic.SearchResult) []*SearchResultLanguages { } // Search searches for codes and language stats by given conditions. -func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { +func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { searchType := esMultiMatchTypeBestFields if isMatch { searchType = esMultiMatchTypePhrasePrefix @@ -412,8 +304,8 @@ func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, lang } if len(language) == 0 { - searchResult, err := b.client.Search(). - Index(b.indexerAliasName). + searchResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). Aggregation("language", aggregation). Query(query). Highlight( @@ -426,26 +318,26 @@ func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, lang From(start).Size(pageSize). Do(ctx) if err != nil { - return 0, nil, nil, b.checkError(err) + return 0, nil, nil, err } return convertResult(searchResult, kw, pageSize) } langQuery := elastic.NewMatchQuery("language", language) - countResult, err := b.client.Search(). - Index(b.indexerAliasName). + countResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). Aggregation("language", aggregation). Query(query). - Size(0). // We only needs stats information + Size(0). // We only need stats information Do(ctx) if err != nil { - return 0, nil, nil, b.checkError(err) + return 0, nil, nil, err } query = query.Must(langQuery) - searchResult, err := b.client.Search(). - Index(b.indexerAliasName). + searchResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). Query(query). Highlight( elastic.NewHighlight(). @@ -457,56 +349,10 @@ func (b *ElasticSearchIndexer) Search(ctx context.Context, repoIDs []int64, lang From(start).Size(pageSize). Do(ctx) if err != nil { - return 0, nil, nil, b.checkError(err) + return 0, nil, nil, err } total, hits, _, err := convertResult(searchResult, kw, pageSize) return total, hits, extractAggs(countResult), err } - -// Close implements indexer -func (b *ElasticSearchIndexer) Close() { - select { - case <-b.stopTimer: - default: - close(b.stopTimer) - } -} - -func (b *ElasticSearchIndexer) checkError(err error) error { - var opErr *net.OpError - if !(elastic.IsConnErr(err) || (errors.As(err, &opErr) && (opErr.Op == "dial" || opErr.Op == "read"))) { - return err - } - - b.setAvailability(false) - - return err -} - -func (b *ElasticSearchIndexer) checkAvailability() { - if b.Ping() { - return - } - - // Request cluster state to check if elastic is available again - _, err := b.client.ClusterState().Do(graceful.GetManager().ShutdownContext()) - if err != nil { - b.setAvailability(false) - return - } - - b.setAvailability(true) -} - -func (b *ElasticSearchIndexer) setAvailability(available bool) { - b.lock.Lock() - defer b.lock.Unlock() - - if b.available == available { - return - } - - b.available = available -} diff --git a/modules/indexer/code/elasticsearch/elasticsearch_test.go b/modules/indexer/code/elasticsearch/elasticsearch_test.go new file mode 100644 index 000000000000..c6ba93e76d46 --- /dev/null +++ b/modules/indexer/code/elasticsearch/elasticsearch_test.go @@ -0,0 +1,16 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIndexPos(t *testing.T) { + startIdx, endIdx := indexPos("test index start and end", "start", "end") + assert.EqualValues(t, 11, startIdx) + assert.EqualValues(t, 24, endIdx) +} diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index bbcc6ba48719..1ba6b849d11d 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -10,23 +10,11 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) -type fileUpdate struct { - Filename string - BlobSha string - Size int64 - Sized bool -} - -// repoChanges changes (file additions/updates/removals) to a repo -type repoChanges struct { - Updates []fileUpdate - RemovedFilenames []string -} - func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (string, error) { stdout, _, err := git.NewCommand(ctx, "show-ref", "-s").AddDynamicArguments(git.BranchPrefix + repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if err != nil { @@ -36,7 +24,7 @@ func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (stri } // getRepoChanges returns changes to repo since last indexer update -func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { +func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { status, err := repo_model.GetIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeCode) if err != nil { return nil, err @@ -67,16 +55,16 @@ func isIndexable(entry *git.TreeEntry) bool { } // parseGitLsTreeOutput parses the output of a `git ls-tree -r --full-name` command -func parseGitLsTreeOutput(stdout []byte) ([]fileUpdate, error) { +func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) { entries, err := git.ParseTreeEntries(stdout) if err != nil { return nil, err } idxCount := 0 - updates := make([]fileUpdate, len(entries)) + updates := make([]internal.FileUpdate, len(entries)) for _, entry := range entries { if isIndexable(entry) { - updates[idxCount] = fileUpdate{ + updates[idxCount] = internal.FileUpdate{ Filename: entry.Name(), BlobSha: entry.ID.String(), Size: entry.Size(), @@ -89,8 +77,8 @@ func parseGitLsTreeOutput(stdout []byte) ([]fileUpdate, error) { } // genesisChanges get changes to add repo to the indexer for the first time -func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { - var changes repoChanges +func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { + var changes internal.RepoChanges stdout, _, runErr := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision).RunStdBytes(&git.RunOpts{Dir: repo.RepoPath()}) if runErr != nil { return nil, runErr @@ -102,20 +90,20 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s } // nonGenesisChanges get changes since the previous indexer update -func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { +func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { diffCmd := git.NewCommand(ctx, "diff", "--name-status").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision) stdout, _, runErr := diffCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if runErr != nil { // previous commit sha may have been removed by a force push, so // try rebuilding from scratch log.Warn("git diff: %v", runErr) - if err := indexer.Delete(repo.ID); err != nil { + if err := (*globalIndexer.Load()).Delete(ctx, repo.ID); err != nil { return nil, err } return genesisChanges(ctx, repo, revision) } - var changes repoChanges + var changes internal.RepoChanges var err error updatedFilenames := make([]string, 0, 10) for _, line := range strings.Split(stdout, "\n") { diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index f38fd6000c70..13d06874c96d 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -7,86 +7,41 @@ import ( "context" "os" "runtime/pprof" - "strconv" - "strings" + "sync/atomic" "time" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/indexer/code/bleve" + "code.gitea.io/gitea/modules/indexer/code/elasticsearch" + "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) -// SearchResult result of performing a search in a repo -type SearchResult struct { - RepoID int64 - StartIndex int - EndIndex int - Filename string - Content string - CommitID string - UpdatedUnix timeutil.TimeStamp - Language string - Color string -} - -// SearchResultLanguages result of top languages count in search results -type SearchResultLanguages struct { - Language string - Color string - Count int -} - -// Indexer defines an interface to index and search code contents -type Indexer interface { - Ping() bool - Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error - Delete(repoID int64) error - Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) - Close() -} - -func filenameIndexerID(repoID int64, filename string) string { - return indexerID(repoID) + "_" + filename -} - -func indexerID(id int64) string { - return strconv.FormatInt(id, 36) -} - -func parseIndexerID(indexerID string) (int64, string) { - index := strings.IndexByte(indexerID, '_') - if index == -1 { - log.Error("Unexpected ID in repo indexer: %s", indexerID) - } - repoID, _ := strconv.ParseInt(indexerID[:index], 36, 64) - return repoID, indexerID[index+1:] -} - -func filenameOfIndexerID(indexerID string) string { - index := strings.IndexByte(indexerID, '_') - if index == -1 { - log.Error("Unexpected ID in repo indexer: %s", indexerID) - } - return indexerID[index+1:] -} +var ( + indexerQueue *queue.WorkerPoolQueue[*internal.IndexerData] + // globalIndexer is the global indexer, it cannot be nil. + // When the real indexer is not ready, it will be a dummy indexer which will return error to explain it's not ready. + // So it's always safe use it as *globalIndexer.Load() and call its methods. + globalIndexer atomic.Pointer[internal.Indexer] + dummyIndexer *internal.Indexer +) -// IndexerData represents data stored in the code indexer -type IndexerData struct { - RepoID int64 +func init() { + i := internal.NewDummyIndexer() + dummyIndexer = &i + globalIndexer.Store(dummyIndexer) } -var indexerQueue *queue.WorkerPoolQueue[*IndexerData] - -func index(ctx context.Context, indexer Indexer, repoID int64) error { +func index(ctx context.Context, indexer internal.Indexer, repoID int64) error { repo, err := repo_model.GetRepositoryByID(ctx, repoID) if repo_model.IsErrRepoNotExist(err) { - return indexer.Delete(repoID) + return indexer.Delete(ctx, repoID) } if err != nil { return err @@ -139,7 +94,7 @@ func index(ctx context.Context, indexer Indexer, repoID int64) error { // Init initialize the repo indexer func Init() { if !setting.Indexer.RepoIndexerEnabled { - indexer.Close() + (*globalIndexer.Load()).Close() return } @@ -153,7 +108,7 @@ func Init() { } cancel() log.Debug("Closing repository indexer") - indexer.Close() + (*globalIndexer.Load()).Close() log.Info("PID: %d Repository Indexer closed", os.Getpid()) finished() }) @@ -163,13 +118,8 @@ func Init() { // Create the Queue switch setting.Indexer.RepoType { case "bleve", "elasticsearch": - handler := func(items ...*IndexerData) (unhandled []*IndexerData) { - idx, err := indexer.get() - if idx == nil || err != nil { - log.Warn("Codes indexer handler: indexer is not ready, retry later.") - return items - } - + handler := func(items ...*internal.IndexerData) (unhandled []*internal.IndexerData) { + indexer := *globalIndexer.Load() for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) @@ -188,11 +138,7 @@ func Init() { code.gitea.io/gitea/modules/indexer/code.index(indexer.go:105) */ if err := index(ctx, indexer, indexerData.RepoID); err != nil { - if !idx.Ping() { - log.Error("Code indexer handler: indexer is unavailable.") - unhandled = append(unhandled, indexerData) - continue - } + unhandled = append(unhandled, indexerData) if !setting.IsInTesting { log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err) } @@ -213,8 +159,8 @@ func Init() { pprof.SetGoroutineLabels(ctx) start := time.Now() var ( - rIndexer Indexer - populate bool + rIndexer internal.Indexer + existed bool err error ) switch setting.Indexer.RepoType { @@ -228,10 +174,11 @@ func Init() { } }() - rIndexer, populate, err = NewBleveIndexer(setting.Indexer.RepoPath) + rIndexer = bleve.NewIndexer(setting.Indexer.RepoPath) + existed, err = rIndexer.Init(ctx) if err != nil { cancel() - indexer.Close() + (*globalIndexer.Load()).Close() close(waitChannel) log.Fatal("PID: %d Unable to initialize the bleve Repository Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.RepoPath, err) } @@ -245,23 +192,31 @@ func Init() { } }() - rIndexer, populate, err = NewElasticSearchIndexer(setting.Indexer.RepoConnStr, setting.Indexer.RepoIndexerName) + rIndexer = elasticsearch.NewIndexer(setting.Indexer.RepoConnStr, setting.Indexer.RepoIndexerName) + if err != nil { + cancel() + (*globalIndexer.Load()).Close() + close(waitChannel) + log.Fatal("PID: %d Unable to create the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err) + } + existed, err = rIndexer.Init(ctx) if err != nil { cancel() - indexer.Close() + (*globalIndexer.Load()).Close() close(waitChannel) log.Fatal("PID: %d Unable to initialize the elasticsearch Repository Indexer connstr: %s Error: %v", os.Getpid(), setting.Indexer.RepoConnStr, err) } + default: log.Fatal("PID: %d Unknown Indexer type: %s", os.Getpid(), setting.Indexer.RepoType) } - indexer.set(rIndexer) + globalIndexer.Store(&rIndexer) // Start processing the queue go graceful.GetManager().RunWithCancel(indexerQueue) - if populate { + if !existed { // populate the index because it's created for the first time go graceful.GetManager().RunWithShutdownContext(populateRepoIndexer) } select { @@ -283,18 +238,18 @@ func Init() { case <-graceful.GetManager().IsShutdown(): log.Warn("Shutdown before Repository Indexer completed initialization") cancel() - indexer.Close() + (*globalIndexer.Load()).Close() case duration, ok := <-waitChannel: if !ok { log.Warn("Repository Indexer Initialization failed") cancel() - indexer.Close() + (*globalIndexer.Load()).Close() return } log.Info("Repository Indexer Initialization took %v", duration) case <-time.After(timeout): cancel() - indexer.Close() + (*globalIndexer.Load()).Close() log.Fatal("Repository Indexer Initialization Timed-Out after: %v", timeout) } }() @@ -303,21 +258,15 @@ func Init() { // UpdateRepoIndexer update a repository's entries in the indexer func UpdateRepoIndexer(repo *repo_model.Repository) { - indexData := &IndexerData{RepoID: repo.ID} + indexData := &internal.IndexerData{RepoID: repo.ID} if err := indexerQueue.Push(indexData); err != nil { log.Error("Update repo index data %v failed: %v", indexData, err) } } // IsAvailable checks if issue indexer is available -func IsAvailable() bool { - idx, err := indexer.get() - if err != nil { - log.Error("IsAvailable(): unable to get indexer: %v", err) - return false - } - - return idx.Ping() +func IsAvailable(ctx context.Context) bool { + return (*globalIndexer.Load()).Ping(ctx) == nil } // populateRepoIndexer populate the repo indexer with pre-existing data. This @@ -368,7 +317,7 @@ func populateRepoIndexer(ctx context.Context) { return default: } - if err := indexerQueue.Push(&IndexerData{RepoID: id}); err != nil { + if err := indexerQueue.Push(&internal.IndexerData{RepoID: id}); err != nil { log.Error("indexerQueue.Push: %v", err) return } diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 52f7e76e413f..55616a0361e2 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -5,11 +5,15 @@ package code import ( "context" + "os" "path/filepath" "testing" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/indexer/code/bleve" + "code.gitea.io/gitea/modules/indexer/code/elasticsearch" + "code.gitea.io/gitea/modules/indexer/code/internal" _ "code.gitea.io/gitea/models" @@ -22,7 +26,7 @@ func TestMain(m *testing.M) { }) } -func testIndexer(name string, t *testing.T, indexer Indexer) { +func testIndexer(name string, t *testing.T, indexer internal.Indexer) { t.Run(name, func(t *testing.T) { var repoID int64 = 1 err := index(git.DefaultContext, indexer, repoID) @@ -81,6 +85,48 @@ func testIndexer(name string, t *testing.T, indexer Indexer) { }) } - assert.NoError(t, indexer.Delete(repoID)) + assert.NoError(t, indexer.Delete(context.Background(), repoID)) }) } + +func TestBleveIndexAndSearch(t *testing.T) { + unittest.PrepareTestEnv(t) + + dir := t.TempDir() + + idx := bleve.NewIndexer(dir) + _, err := idx.Init(context.Background()) + if err != nil { + assert.Fail(t, "Unable to create bleve indexer Error: %v", err) + if idx != nil { + idx.Close() + } + return + } + defer idx.Close() + + testIndexer("beleve", t, idx) +} + +func TestESIndexAndSearch(t *testing.T) { + unittest.PrepareTestEnv(t) + + u := os.Getenv("TEST_INDEXER_CODE_ES_URL") + if u == "" { + t.SkipNow() + return + } + + indexer := elasticsearch.NewIndexer(u, "gitea_codes") + if _, err := indexer.Init(context.Background()); err != nil { + assert.Fail(t, "Unable to init ES indexer Error: %v", err) + if indexer != nil { + indexer.Close() + } + return + } + + defer indexer.Close() + + testIndexer("elastic_search", t, indexer) +} diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go new file mode 100644 index 000000000000..da3ac3623c92 --- /dev/null +++ b/modules/indexer/code/internal/indexer.go @@ -0,0 +1,43 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "context" + "fmt" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/indexer/internal" +) + +// Indexer defines an interface to index and search code contents +type Indexer interface { + internal.Indexer + Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error + Delete(ctx context.Context, repoID int64) error + Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) +} + +// NewDummyIndexer returns a dummy indexer +func NewDummyIndexer() Indexer { + return &dummyIndexer{ + Indexer: internal.NewDummyIndexer(), + } +} + +type dummyIndexer struct { + internal.Indexer +} + +func (d *dummyIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { + return 0, nil, nil, fmt.Errorf("indexer is not ready") +} diff --git a/modules/indexer/code/internal/model.go b/modules/indexer/code/internal/model.go new file mode 100644 index 000000000000..f75263c83cfe --- /dev/null +++ b/modules/indexer/code/internal/model.go @@ -0,0 +1,44 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import "code.gitea.io/gitea/modules/timeutil" + +type FileUpdate struct { + Filename string + BlobSha string + Size int64 + Sized bool +} + +// RepoChanges changes (file additions/updates/removals) to a repo +type RepoChanges struct { + Updates []FileUpdate + RemovedFilenames []string +} + +// IndexerData represents data stored in the code indexer +type IndexerData struct { + RepoID int64 +} + +// SearchResult result of performing a search in a repo +type SearchResult struct { + RepoID int64 + StartIndex int + EndIndex int + Filename string + Content string + CommitID string + UpdatedUnix timeutil.TimeStamp + Language string + Color string +} + +// SearchResultLanguages result of top languages count in search results +type SearchResultLanguages struct { + Language string + Color string + Count int +} diff --git a/modules/indexer/code/internal/util.go b/modules/indexer/code/internal/util.go new file mode 100644 index 000000000000..689c4f4584b1 --- /dev/null +++ b/modules/indexer/code/internal/util.go @@ -0,0 +1,32 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "strings" + + "code.gitea.io/gitea/modules/indexer/internal" + "code.gitea.io/gitea/modules/log" +) + +func FilenameIndexerID(repoID int64, filename string) string { + return internal.Base36(repoID) + "_" + filename +} + +func ParseIndexerID(indexerID string) (int64, string) { + index := strings.IndexByte(indexerID, '_') + if index == -1 { + log.Error("Unexpected ID in repo indexer: %s", indexerID) + } + repoID, _ := internal.ParseBase36(indexerID[:index]) + return repoID, indexerID[index+1:] +} + +func FilenameOfIndexerID(indexerID string) string { + index := strings.IndexByte(indexerID, '_') + if index == -1 { + log.Error("Unexpected ID in repo indexer: %s", indexerID) + } + return indexerID[index+1:] +} diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index 1de9ffc224b1..1f9bddff7b10 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -25,6 +26,8 @@ type Result struct { FormattedLines string } +type SearchResultLanguages = internal.SearchResultLanguages + func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { startIndex := selectionStartIndex numLinesBefore := 0 @@ -61,7 +64,7 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error { return nil } -func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, error) { +func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) { startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n") var formattedLinesBuffer bytes.Buffer @@ -109,12 +112,12 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro } // PerformSearch perform a search on a repository -func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*SearchResultLanguages, error) { +func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*internal.SearchResultLanguages, error) { if len(keyword) == 0 { return 0, nil, nil, nil } - total, results, resultLanguages, err := indexer.Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) + total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) if err != nil { return 0, nil, nil, err } diff --git a/modules/indexer/code/wrapped.go b/modules/indexer/code/wrapped.go deleted file mode 100644 index 7eed3e855759..000000000000 --- a/modules/indexer/code/wrapped.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package code - -import ( - "context" - "fmt" - "sync" - - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/log" -) - -var indexer = newWrappedIndexer() - -// ErrWrappedIndexerClosed is the error returned if the indexer was closed before it was ready -var ErrWrappedIndexerClosed = fmt.Errorf("Indexer closed before ready") - -type wrappedIndexer struct { - internal Indexer - lock sync.RWMutex - cond *sync.Cond - closed bool -} - -func newWrappedIndexer() *wrappedIndexer { - w := &wrappedIndexer{} - w.cond = sync.NewCond(w.lock.RLocker()) - return w -} - -func (w *wrappedIndexer) set(indexer Indexer) { - w.lock.Lock() - defer w.lock.Unlock() - if w.closed { - // Too late! - indexer.Close() - } - w.internal = indexer - w.cond.Broadcast() -} - -func (w *wrappedIndexer) get() (Indexer, error) { - w.lock.RLock() - defer w.lock.RUnlock() - if w.internal == nil { - if w.closed { - return nil, ErrWrappedIndexerClosed - } - w.cond.Wait() - if w.closed { - return nil, ErrWrappedIndexerClosed - } - } - return w.internal, nil -} - -// Ping checks if elastic is available -func (w *wrappedIndexer) Ping() bool { - indexer, err := w.get() - if err != nil { - log.Warn("Failed to get indexer: %v", err) - return false - } - return indexer.Ping() -} - -func (w *wrappedIndexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *repoChanges) error { - indexer, err := w.get() - if err != nil { - return err - } - return indexer.Index(ctx, repo, sha, changes) -} - -func (w *wrappedIndexer) Delete(repoID int64) error { - indexer, err := w.get() - if err != nil { - return err - } - return indexer.Delete(repoID) -} - -func (w *wrappedIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { - indexer, err := w.get() - if err != nil { - return 0, nil, nil, err - } - return indexer.Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) -} - -func (w *wrappedIndexer) Close() { - w.lock.Lock() - defer w.lock.Unlock() - if w.closed { - return - } - w.closed = true - w.cond.Broadcast() - if w.internal != nil { - w.internal.Close() - } -} diff --git a/modules/indexer/internal/base32.go b/modules/indexer/internal/base32.go new file mode 100644 index 000000000000..aca756c638a8 --- /dev/null +++ b/modules/indexer/internal/base32.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "fmt" + "strconv" +) + +func Base36(i int64) string { + return strconv.FormatInt(i, 36) +} + +func ParseBase36(s string) (int64, error) { + i, err := strconv.ParseInt(s, 36, 64) + if err != nil { + return 0, fmt.Errorf("invalid base36 integer %q: %w", s, err) + } + return i, nil +} diff --git a/modules/indexer/bleve/batch.go b/modules/indexer/internal/bleve/batch.go similarity index 100% rename from modules/indexer/bleve/batch.go rename to modules/indexer/internal/bleve/batch.go diff --git a/modules/indexer/internal/bleve/indexer.go b/modules/indexer/internal/bleve/indexer.go new file mode 100644 index 000000000000..ce06b5afcb78 --- /dev/null +++ b/modules/indexer/internal/bleve/indexer.go @@ -0,0 +1,103 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package bleve + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/modules/indexer/internal" + "code.gitea.io/gitea/modules/log" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/mapping" + "github.com/ethantkoenig/rupture" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer represents a basic bleve indexer implementation +type Indexer struct { + Indexer bleve.Index + + indexDir string + version int + mappingGetter MappingGetter +} + +type MappingGetter func() (mapping.IndexMapping, error) + +func NewIndexer(indexDir string, version int, mappingGetter func() (mapping.IndexMapping, error)) *Indexer { + return &Indexer{ + indexDir: indexDir, + version: version, + mappingGetter: mappingGetter, + } +} + +// Init initializes the indexer +func (i *Indexer) Init(_ context.Context) (bool, error) { + if i == nil { + return false, fmt.Errorf("cannot init nil indexer") + } + + if i.Indexer != nil { + return false, fmt.Errorf("indexer is already initialized") + } + + indexer, version, err := openIndexer(i.indexDir, i.version) + if err != nil { + return false, err + } + if indexer != nil { + i.Indexer = indexer + return true, nil + } + + if version != 0 { + log.Warn("Found older bleve index with version %d, Gitea will remove it and rebuild", version) + } + + indexMapping, err := i.mappingGetter() + if err != nil { + return false, err + } + + indexer, err = bleve.New(i.indexDir, indexMapping) + if err != nil { + return false, err + } + + if err = rupture.WriteIndexMetadata(i.indexDir, &rupture.IndexMetadata{ + Version: i.version, + }); err != nil { + return false, err + } + + i.Indexer = indexer + + return false, nil +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(_ context.Context) error { + if i == nil { + return fmt.Errorf("cannot ping nil indexer") + } + if i.Indexer == nil { + return fmt.Errorf("indexer is not initialized") + } + return nil +} + +func (i *Indexer) Close() { + if i == nil { + return + } + + if err := i.Indexer.Close(); err != nil { + log.Error("Failed to close bleve indexer in %q: %v", i.indexDir, err) + } + i.Indexer = nil +} diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go new file mode 100644 index 000000000000..43a7c3c5ec1b --- /dev/null +++ b/modules/indexer/internal/bleve/util.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package bleve + +import ( + "errors" + "os" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + + "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/index/upsidedown" + "github.com/ethantkoenig/rupture" +) + +// openIndexer open the index at the specified path, checking for metadata +// updates and bleve version updates. If index needs to be created (or +// re-created), returns (nil, nil) +func openIndexer(path string, latestVersion int) (bleve.Index, int, error) { + _, err := os.Stat(path) + if err != nil && os.IsNotExist(err) { + return nil, 0, nil + } else if err != nil { + return nil, 0, err + } + + metadata, err := rupture.ReadIndexMetadata(path) + if err != nil { + return nil, 0, err + } + if metadata.Version < latestVersion { + // the indexer is using a previous version, so we should delete it and + // re-populate + return nil, metadata.Version, util.RemoveAll(path) + } + + index, err := bleve.Open(path) + if err != nil { + if errors.Is(err, upsidedown.IncompatibleVersion) { + log.Warn("Indexer was built with a previous version of bleve, deleting and rebuilding") + return nil, 0, util.RemoveAll(path) + } + return nil, 0, err + } + + return index, 0, nil +} diff --git a/modules/indexer/internal/db/indexer.go b/modules/indexer/internal/db/indexer.go new file mode 100644 index 000000000000..3deec836c4e6 --- /dev/null +++ b/modules/indexer/internal/db/indexer.go @@ -0,0 +1,34 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "context" + + "code.gitea.io/gitea/modules/indexer/internal" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer represents a basic db indexer implementation +type Indexer struct{} + +// Init initializes the indexer +func (i *Indexer) Init(_ context.Context) (bool, error) { + // Return true to indicate that the index was opened/existed. + // So that the indexer will not try to populate the index, the data is already there. + return true, nil +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(_ context.Context) error { + // No need to ping database to check if it is available. + // If the database goes down, Gitea will go down, so nobody will care if the indexer is available. + return nil +} + +// Close closes the indexer +func (i *Indexer) Close() { + // nothing to do +} diff --git a/modules/indexer/internal/elasticsearch/indexer.go b/modules/indexer/internal/elasticsearch/indexer.go new file mode 100644 index 000000000000..2c60efad564f --- /dev/null +++ b/modules/indexer/internal/elasticsearch/indexer.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/modules/indexer/internal" + + "github.com/olivere/elastic/v7" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer represents a basic elasticsearch indexer implementation +type Indexer struct { + Client *elastic.Client + + url string + indexName string + version int + mapping string +} + +func NewIndexer(url, indexName string, version int, mapping string) *Indexer { + return &Indexer{ + url: url, + indexName: indexName, + version: version, + mapping: mapping, + } +} + +// Init initializes the indexer +func (i *Indexer) Init(ctx context.Context) (bool, error) { + if i == nil { + return false, fmt.Errorf("cannot init nil indexer") + } + if i.Client != nil { + return false, fmt.Errorf("indexer is already initialized") + } + + client, err := i.initClient() + if err != nil { + return false, err + } + i.Client = client + + exists, err := i.Client.IndexExists(i.VersionedIndexName()).Do(ctx) + if err != nil { + return false, err + } + if exists { + return true, nil + } + + if err := i.createIndex(ctx); err != nil { + return false, err + } + + return exists, nil +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(ctx context.Context) error { + if i == nil { + return fmt.Errorf("cannot ping nil indexer") + } + if i.Client == nil { + return fmt.Errorf("indexer is not initialized") + } + + resp, err := i.Client.ClusterHealth().Do(ctx) + if err != nil { + return err + } + if resp.Status != "green" { + // see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html + return fmt.Errorf("status of elasticsearch cluster is %s", resp.Status) + } + return nil +} + +// Close closes the indexer +func (i *Indexer) Close() { + if i == nil { + return + } + i.Client = nil +} diff --git a/modules/indexer/internal/elasticsearch/util.go b/modules/indexer/internal/elasticsearch/util.go new file mode 100644 index 000000000000..9e034bd55309 --- /dev/null +++ b/modules/indexer/internal/elasticsearch/util.go @@ -0,0 +1,68 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "context" + "fmt" + "time" + + "code.gitea.io/gitea/modules/log" + + "github.com/olivere/elastic/v7" +) + +// VersionedIndexName returns the full index name with version +func (i *Indexer) VersionedIndexName() string { + return versionedIndexName(i.indexName, i.version) +} + +func versionedIndexName(indexName string, version int) string { + if version == 0 { + // Old index name without version + return indexName + } + return fmt.Sprintf("%s.v%d", indexName, version) +} + +func (i *Indexer) createIndex(ctx context.Context) error { + createIndex, err := i.Client.CreateIndex(i.VersionedIndexName()).BodyString(i.mapping).Do(ctx) + if err != nil { + return err + } + if !createIndex.Acknowledged { + return fmt.Errorf("create index %s with %s failed", i.VersionedIndexName(), i.mapping) + } + + i.checkOldIndexes(ctx) + + return nil +} + +func (i *Indexer) initClient() (*elastic.Client, error) { + opts := []elastic.ClientOptionFunc{ + elastic.SetURL(i.url), + elastic.SetSniff(false), + elastic.SetHealthcheckInterval(10 * time.Second), + elastic.SetGzip(false), + } + + logger := log.GetLogger(log.DEFAULT) + + opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace})) + opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info})) + opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error})) + + return elastic.NewClient(opts...) +} + +func (i *Indexer) checkOldIndexes(ctx context.Context) { + for v := 0; v < i.version; v++ { + indexName := versionedIndexName(i.indexName, v) + exists, err := i.Client.IndexExists(indexName).Do(ctx) + if err == nil && exists { + log.Warn("Found older elasticsearch index named %q, Gitea will keep the old NOT DELETED. You can delete the old version after the upgrade succeed.", indexName) + } + } +} diff --git a/modules/indexer/internal/indexer.go b/modules/indexer/internal/indexer.go new file mode 100644 index 000000000000..c7f356da1efe --- /dev/null +++ b/modules/indexer/internal/indexer.go @@ -0,0 +1,37 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "context" + "fmt" +) + +// Indexer defines an basic indexer interface +type Indexer interface { + // Init initializes the indexer + // returns true if the index was opened/existed (with data populated), false if it was created/not-existed (with no data) + Init(ctx context.Context) (bool, error) + // Ping checks if the indexer is available + Ping(ctx context.Context) error + // Close closes the indexer + Close() +} + +// NewDummyIndexer returns a dummy indexer +func NewDummyIndexer() Indexer { + return &dummyIndexer{} +} + +type dummyIndexer struct{} + +func (d *dummyIndexer) Init(ctx context.Context) (bool, error) { + return false, fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Ping(ctx context.Context) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Close() {} diff --git a/modules/indexer/internal/meilisearch/indexer.go b/modules/indexer/internal/meilisearch/indexer.go new file mode 100644 index 000000000000..06747ff7e07a --- /dev/null +++ b/modules/indexer/internal/meilisearch/indexer.go @@ -0,0 +1,92 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package meilisearch + +import ( + "context" + "fmt" + + "github.com/meilisearch/meilisearch-go" +) + +// Indexer represents a basic meilisearch indexer implementation +type Indexer struct { + Client *meilisearch.Client + + url, apiKey string + indexName string + version int +} + +func NewIndexer(url, apiKey, indexName string, version int) *Indexer { + return &Indexer{ + url: url, + apiKey: apiKey, + indexName: indexName, + version: version, + } +} + +// Init initializes the indexer +func (i *Indexer) Init(_ context.Context) (bool, error) { + if i == nil { + return false, fmt.Errorf("cannot init nil indexer") + } + + if i.Client != nil { + return false, fmt.Errorf("indexer is already initialized") + } + + i.Client = meilisearch.NewClient(meilisearch.ClientConfig{ + Host: i.url, + APIKey: i.apiKey, + }) + + _, err := i.Client.GetIndex(i.VersionedIndexName()) + if err == nil { + return true, nil + } + _, err = i.Client.CreateIndex(&meilisearch.IndexConfig{ + Uid: i.VersionedIndexName(), + PrimaryKey: "id", + }) + if err != nil { + return false, err + } + + i.checkOldIndexes() + + _, err = i.Client.Index(i.VersionedIndexName()).UpdateFilterableAttributes(&[]string{"repo_id"}) + return false, err +} + +// Ping checks if the indexer is available +func (i *Indexer) Ping(ctx context.Context) error { + if i == nil { + return fmt.Errorf("cannot ping nil indexer") + } + if i.Client == nil { + return fmt.Errorf("indexer is not initialized") + } + resp, err := i.Client.Health() + if err != nil { + return err + } + if resp.Status != "available" { + // See https://docs.meilisearch.com/reference/api/health.html#status + return fmt.Errorf("status of meilisearch is not available: %s", resp.Status) + } + return nil +} + +// Close closes the indexer +func (i *Indexer) Close() { + if i == nil { + return + } + if i.Client == nil { + return + } + i.Client = nil +} diff --git a/modules/indexer/internal/meilisearch/util.go b/modules/indexer/internal/meilisearch/util.go new file mode 100644 index 000000000000..e6d8fefadeb8 --- /dev/null +++ b/modules/indexer/internal/meilisearch/util.go @@ -0,0 +1,38 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package meilisearch + +import ( + "fmt" + + "code.gitea.io/gitea/modules/log" +) + +// VersionedIndexName returns the full index name with version +func (i *Indexer) VersionedIndexName() string { + return versionedIndexName(i.indexName, i.version) +} + +func versionedIndexName(indexName string, version int) string { + if version == 0 { + // Old index name without version + return indexName + } + + // The format of the index name is _v, not .v like elasticsearch. + // Because meilisearch does not support "." in index name, it should contain only alphanumeric characters, hyphens (-) and underscores (_). + // See https://www.meilisearch.com/docs/learn/core_concepts/indexes#index-uid + + return fmt.Sprintf("%s_v%d", indexName, version) +} + +func (i *Indexer) checkOldIndexes() { + for v := 0; v < i.version; v++ { + indexName := versionedIndexName(i.indexName, v) + _, err := i.Client.GetIndex(indexName) + if err == nil { + log.Warn("Found older meilisearch index named %q, Gitea will keep the old NOT DELETED. You can delete the old version after the upgrade succeed.", indexName) + } + } +} diff --git a/modules/indexer/issues/bleve.go b/modules/indexer/issues/bleve/bleve.go similarity index 52% rename from modules/indexer/issues/bleve.go rename to modules/indexer/issues/bleve/bleve.go index 60d9ef76174f..bb0bc4b04a41 100644 --- a/modules/indexer/issues/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -1,17 +1,14 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package issues +package bleve import ( "context" - "fmt" - "os" - "strconv" - gitea_bleve "code.gitea.io/gitea/modules/indexer/bleve" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" + "code.gitea.io/gitea/modules/indexer/issues/internal" "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" @@ -19,10 +16,8 @@ import ( "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" - "github.com/blevesearch/bleve/v2/index/upsidedown" "github.com/blevesearch/bleve/v2/mapping" "github.com/blevesearch/bleve/v2/search/query" - "github.com/ethantkoenig/rupture" ) const ( @@ -31,20 +26,6 @@ const ( issueIndexerLatestVersion = 2 ) -// indexerID a bleve-compatible unique identifier for an integer id -func indexerID(id int64) string { - return strconv.FormatInt(id, 36) -} - -// idOfIndexerID the integer id associated with an indexer id -func idOfIndexerID(indexerID string) (int64, error) { - id, err := strconv.ParseInt(indexerID, 36, 64) - if err != nil { - return 0, fmt.Errorf("Unexpected indexer ID %s: %w", indexerID, err) - } - return id, nil -} - // numericEqualityQuery a numeric equality query for the given value and field func numericEqualityQuery(value int64, field string) *query.NumericRangeQuery { f := float64(value) @@ -72,49 +53,16 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { const maxBatchSize = 16 -// openIndexer open the index at the specified path, checking for metadata -// updates and bleve version updates. If index needs to be created (or -// re-created), returns (nil, nil) -func openIndexer(path string, latestVersion int) (bleve.Index, error) { - _, err := os.Stat(path) - if err != nil && os.IsNotExist(err) { - return nil, nil - } else if err != nil { - return nil, err - } - - metadata, err := rupture.ReadIndexMetadata(path) - if err != nil { - return nil, err - } - if metadata.Version < latestVersion { - // the indexer is using a previous version, so we should delete it and - // re-populate - return nil, util.RemoveAll(path) - } - - index, err := bleve.Open(path) - if err != nil && err == upsidedown.IncompatibleVersion { - // the indexer was built with a previous version of bleve, so we should - // delete it and re-populate - return nil, util.RemoveAll(path) - } else if err != nil { - return nil, err - } - - return index, nil -} - -// BleveIndexerData an update to the issue indexer -type BleveIndexerData IndexerData +// IndexerData an update to the issue indexer +type IndexerData internal.IndexerData // Type returns the document type, for bleve's mapping.Classifier interface. -func (i *BleveIndexerData) Type() string { +func (i *IndexerData) Type() string { return issueIndexerDocType } -// createIssueIndexer create an issue indexer if one does not already exist -func createIssueIndexer(path string, latestVersion int) (bleve.Index, error) { +// generateIssueIndexMapping generates the bleve index mapping for issues +func generateIssueIndexMapping() (mapping.IndexMapping, error) { mapping := bleve.NewIndexMapping() docMapping := bleve.NewDocumentMapping() @@ -144,68 +92,31 @@ func createIssueIndexer(path string, latestVersion int) (bleve.Index, error) { mapping.AddDocumentMapping(issueIndexerDocType, docMapping) mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping()) - index, err := bleve.New(path, mapping) - if err != nil { - return nil, err - } - - if err = rupture.WriteIndexMetadata(path, &rupture.IndexMetadata{ - Version: latestVersion, - }); err != nil { - return nil, err - } - return index, nil + return mapping, nil } -var _ Indexer = &BleveIndexer{} +var _ internal.Indexer = &Indexer{} -// BleveIndexer implements Indexer interface -type BleveIndexer struct { - indexDir string - indexer bleve.Index -} - -// NewBleveIndexer creates a new bleve local indexer -func NewBleveIndexer(indexDir string) *BleveIndexer { - return &BleveIndexer{ - indexDir: indexDir, - } +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_bleve.Indexer + indexer_internal.Indexer // do not composite inner_bleve.Indexer directly to avoid exposing too much } -// Init will initialize the indexer -func (b *BleveIndexer) Init() (bool, error) { - var err error - b.indexer, err = openIndexer(b.indexDir, issueIndexerLatestVersion) - if err != nil { - return false, err - } - if b.indexer != nil { - return true, nil - } - - b.indexer, err = createIssueIndexer(b.indexDir, issueIndexerLatestVersion) - return false, err -} - -// Ping does nothing -func (b *BleveIndexer) Ping() bool { - return true -} - -// Close will close the bleve indexer -func (b *BleveIndexer) Close() { - if b.indexer != nil { - if err := b.indexer.Close(); err != nil { - log.Error("Error whilst closing indexer: %v", err) - } +// NewIndexer creates a new bleve local indexer +func NewIndexer(indexDir string) *Indexer { + inner := inner_bleve.NewIndexer(indexDir, issueIndexerLatestVersion, generateIssueIndexMapping) + return &Indexer{ + Indexer: inner, + inner: inner, } } // Index will save the index data -func (b *BleveIndexer) Index(issues []*IndexerData) error { - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) +func (b *Indexer) Index(_ context.Context, issues []*internal.IndexerData) error { + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) for _, issue := range issues { - if err := batch.Index(indexerID(issue.ID), struct { + if err := batch.Index(indexer_internal.Base36(issue.ID), struct { RepoID int64 Title string Content string @@ -223,10 +134,10 @@ func (b *BleveIndexer) Index(issues []*IndexerData) error { } // Delete deletes indexes by ids -func (b *BleveIndexer) Delete(ids ...int64) error { - batch := gitea_bleve.NewFlushingBatch(b.indexer, maxBatchSize) +func (b *Indexer) Delete(_ context.Context, ids ...int64) error { + batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) for _, id := range ids { - if err := batch.Delete(indexerID(id)); err != nil { + if err := batch.Delete(indexer_internal.Base36(id)); err != nil { return err } } @@ -235,7 +146,7 @@ func (b *BleveIndexer) Delete(ids ...int64) error { // Search searches for issues by given conditions. // Returns the matching issue IDs -func (b *BleveIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { +func (b *Indexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { var repoQueriesP []*query.NumericRangeQuery for _, repoID := range repoIDs { repoQueriesP = append(repoQueriesP, numericEqualityQuery(repoID, "RepoID")) @@ -255,20 +166,20 @@ func (b *BleveIndexer) Search(ctx context.Context, keyword string, repoIDs []int search := bleve.NewSearchRequestOptions(indexerQuery, limit, start, false) search.SortBy([]string{"-_score"}) - result, err := b.indexer.SearchInContext(ctx, search) + result, err := b.inner.Indexer.SearchInContext(ctx, search) if err != nil { return nil, err } - ret := SearchResult{ - Hits: make([]Match, 0, len(result.Hits)), + ret := internal.SearchResult{ + Hits: make([]internal.Match, 0, len(result.Hits)), } for _, hit := range result.Hits { - id, err := idOfIndexerID(hit.ID) + id, err := indexer_internal.ParseBase36(hit.ID) if err != nil { return nil, err } - ret.Hits = append(ret.Hits, Match{ + ret.Hits = append(ret.Hits, internal.Match{ ID: id, }) } diff --git a/modules/indexer/issues/bleve_test.go b/modules/indexer/issues/bleve/bleve_test.go similarity index 86% rename from modules/indexer/issues/bleve_test.go rename to modules/indexer/issues/bleve/bleve_test.go index 22827158e4c0..f890f8eb488f 100644 --- a/modules/indexer/issues/bleve_test.go +++ b/modules/indexer/issues/bleve/bleve_test.go @@ -1,26 +1,28 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package issues +package bleve import ( "context" "testing" + "code.gitea.io/gitea/modules/indexer/issues/internal" + "github.com/stretchr/testify/assert" ) func TestBleveIndexAndSearch(t *testing.T) { dir := t.TempDir() - indexer := NewBleveIndexer(dir) + indexer := NewIndexer(dir) defer indexer.Close() - if _, err := indexer.Init(); err != nil { + if _, err := indexer.Init(context.Background()); err != nil { assert.Fail(t, "Unable to initialize bleve indexer: %v", err) return } - err := indexer.Index([]*IndexerData{ + err := indexer.Index(context.Background(), []*internal.IndexerData{ { ID: 1, RepoID: 2, diff --git a/modules/indexer/issues/db.go b/modules/indexer/issues/db.go deleted file mode 100644 index 04c101c35690..000000000000 --- a/modules/indexer/issues/db.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package issues - -import ( - "context" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" -) - -// DBIndexer implements Indexer interface to use database's like search -type DBIndexer struct{} - -// Init dummy function -func (i *DBIndexer) Init() (bool, error) { - return false, nil -} - -// Ping checks if database is available -func (i *DBIndexer) Ping() bool { - return db.GetEngine(db.DefaultContext).Ping() != nil -} - -// Index dummy function -func (i *DBIndexer) Index(issue []*IndexerData) error { - return nil -} - -// Delete dummy function -func (i *DBIndexer) Delete(ids ...int64) error { - return nil -} - -// Close dummy function -func (i *DBIndexer) Close() { -} - -// Search dummy function -func (i *DBIndexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) { - total, ids, err := issues_model.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start) - if err != nil { - return nil, err - } - result := SearchResult{ - Total: total, - Hits: make([]Match, 0, limit), - } - for _, id := range ids { - result.Hits = append(result.Hits, Match{ - ID: id, - }) - } - return &result, nil -} diff --git a/modules/indexer/issues/db/db.go b/modules/indexer/issues/db/db.go new file mode 100644 index 000000000000..17ed426b384b --- /dev/null +++ b/modules/indexer/issues/db/db.go @@ -0,0 +1,54 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package db + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_db "code.gitea.io/gitea/modules/indexer/internal/db" + "code.gitea.io/gitea/modules/indexer/issues/internal" +) + +var _ internal.Indexer = &Indexer{} + +// Indexer implements Indexer interface to use database's like search +type Indexer struct { + indexer_internal.Indexer +} + +func NewIndexer() *Indexer { + return &Indexer{ + Indexer: &inner_db.Indexer{}, + } +} + +// Index dummy function +func (i *Indexer) Index(_ context.Context, _ []*internal.IndexerData) error { + return nil +} + +// Delete dummy function +func (i *Indexer) Delete(_ context.Context, _ ...int64) error { + return nil +} + +// Search searches for issues +func (i *Indexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { + total, ids, err := issues_model.SearchIssueIDsByKeyword(ctx, kw, repoIDs, limit, start) + if err != nil { + return nil, err + } + result := internal.SearchResult{ + Total: total, + Hits: make([]internal.Match, 0, limit), + } + for _, id := range ids { + result.Hits = append(result.Hits, internal.Match{ + ID: id, + }) + } + return &result, nil +} diff --git a/modules/indexer/issues/elastic_search.go b/modules/indexer/issues/elastic_search.go deleted file mode 100644 index ec62f857adac..000000000000 --- a/modules/indexer/issues/elastic_search.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package issues - -import ( - "context" - "errors" - "fmt" - "net" - "strconv" - "sync" - "time" - - "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/log" - - "github.com/olivere/elastic/v7" -) - -var _ Indexer = &ElasticSearchIndexer{} - -// ElasticSearchIndexer implements Indexer interface -type ElasticSearchIndexer struct { - client *elastic.Client - indexerName string - available bool - stopTimer chan struct{} - lock sync.RWMutex -} - -// NewElasticSearchIndexer creates a new elasticsearch indexer -func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, error) { - opts := []elastic.ClientOptionFunc{ - elastic.SetURL(url), - elastic.SetSniff(false), - elastic.SetHealthcheckInterval(10 * time.Second), - elastic.SetGzip(false), - } - - logger := log.GetLogger(log.DEFAULT) - opts = append(opts, elastic.SetTraceLog(&log.PrintfLogger{Logf: logger.Trace})) - opts = append(opts, elastic.SetInfoLog(&log.PrintfLogger{Logf: logger.Info})) - opts = append(opts, elastic.SetErrorLog(&log.PrintfLogger{Logf: logger.Error})) - - client, err := elastic.NewClient(opts...) - if err != nil { - return nil, err - } - - indexer := &ElasticSearchIndexer{ - client: client, - indexerName: indexerName, - available: true, - stopTimer: make(chan struct{}), - } - - ticker := time.NewTicker(10 * time.Second) - go func() { - for { - select { - case <-ticker.C: - indexer.checkAvailability() - case <-indexer.stopTimer: - ticker.Stop() - return - } - } - }() - - return indexer, nil -} - -const ( - defaultMapping = `{ - "mappings": { - "properties": { - "id": { - "type": "integer", - "index": true - }, - "repo_id": { - "type": "integer", - "index": true - }, - "title": { - "type": "text", - "index": true - }, - "content": { - "type": "text", - "index": true - }, - "comments": { - "type" : "text", - "index": true - } - } - } - }` -) - -// Init will initialize the indexer -func (b *ElasticSearchIndexer) Init() (bool, error) { - ctx := graceful.GetManager().HammerContext() - exists, err := b.client.IndexExists(b.indexerName).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - - if !exists { - mapping := defaultMapping - - createIndex, err := b.client.CreateIndex(b.indexerName).BodyString(mapping).Do(ctx) - if err != nil { - return false, b.checkError(err) - } - if !createIndex.Acknowledged { - return false, errors.New("init failed") - } - - return false, nil - } - return true, nil -} - -// Ping checks if elastic is available -func (b *ElasticSearchIndexer) Ping() bool { - b.lock.RLock() - defer b.lock.RUnlock() - return b.available -} - -// Index will save the index data -func (b *ElasticSearchIndexer) Index(issues []*IndexerData) error { - if len(issues) == 0 { - return nil - } else if len(issues) == 1 { - issue := issues[0] - _, err := b.client.Index(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", issue.ID)). - BodyJson(map[string]interface{}{ - "id": issue.ID, - "repo_id": issue.RepoID, - "title": issue.Title, - "content": issue.Content, - "comments": issue.Comments, - }). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) - } - - reqs := make([]elastic.BulkableRequest, 0) - for _, issue := range issues { - reqs = append(reqs, - elastic.NewBulkIndexRequest(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", issue.ID)). - Doc(map[string]interface{}{ - "id": issue.ID, - "repo_id": issue.RepoID, - "title": issue.Title, - "content": issue.Content, - "comments": issue.Comments, - }), - ) - } - - _, err := b.client.Bulk(). - Index(b.indexerName). - Add(reqs...). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) -} - -// Delete deletes indexes by ids -func (b *ElasticSearchIndexer) Delete(ids ...int64) error { - if len(ids) == 0 { - return nil - } else if len(ids) == 1 { - _, err := b.client.Delete(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", ids[0])). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) - } - - reqs := make([]elastic.BulkableRequest, 0) - for _, id := range ids { - reqs = append(reqs, - elastic.NewBulkDeleteRequest(). - Index(b.indexerName). - Id(fmt.Sprintf("%d", id)), - ) - } - - _, err := b.client.Bulk(). - Index(b.indexerName). - Add(reqs...). - Do(graceful.GetManager().HammerContext()) - return b.checkError(err) -} - -// Search searches for issues by given conditions. -// Returns the matching issue IDs -func (b *ElasticSearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { - kwQuery := elastic.NewMultiMatchQuery(keyword, "title", "content", "comments") - query := elastic.NewBoolQuery() - query = query.Must(kwQuery) - if len(repoIDs) > 0 { - repoStrs := make([]interface{}, 0, len(repoIDs)) - for _, repoID := range repoIDs { - repoStrs = append(repoStrs, repoID) - } - repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) - query = query.Must(repoQuery) - } - searchResult, err := b.client.Search(). - Index(b.indexerName). - Query(query). - Sort("_score", false). - From(start).Size(limit). - Do(ctx) - if err != nil { - return nil, b.checkError(err) - } - - hits := make([]Match, 0, limit) - for _, hit := range searchResult.Hits.Hits { - id, _ := strconv.ParseInt(hit.Id, 10, 64) - hits = append(hits, Match{ - ID: id, - }) - } - - return &SearchResult{ - Total: searchResult.TotalHits(), - Hits: hits, - }, nil -} - -// Close implements indexer -func (b *ElasticSearchIndexer) Close() { - select { - case <-b.stopTimer: - default: - close(b.stopTimer) - } -} - -func (b *ElasticSearchIndexer) checkError(err error) error { - var opErr *net.OpError - if !(elastic.IsConnErr(err) || (errors.As(err, &opErr) && (opErr.Op == "dial" || opErr.Op == "read"))) { - return err - } - - b.setAvailability(false) - - return err -} - -func (b *ElasticSearchIndexer) checkAvailability() { - if b.Ping() { - return - } - - // Request cluster state to check if elastic is available again - _, err := b.client.ClusterState().Do(graceful.GetManager().ShutdownContext()) - if err != nil { - b.setAvailability(false) - return - } - - b.setAvailability(true) -} - -func (b *ElasticSearchIndexer) setAvailability(available bool) { - b.lock.Lock() - defer b.lock.Unlock() - - if b.available == available { - return - } - - b.available = available -} diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go new file mode 100644 index 000000000000..33a7dfc21e0c --- /dev/null +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -0,0 +1,177 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package elasticsearch + +import ( + "context" + "fmt" + "strconv" + + "code.gitea.io/gitea/modules/graceful" + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch" + "code.gitea.io/gitea/modules/indexer/issues/internal" + + "github.com/olivere/elastic/v7" +) + +const ( + issueIndexerLatestVersion = 0 +) + +var _ internal.Indexer = &Indexer{} + +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_elasticsearch.Indexer + indexer_internal.Indexer // do not composite inner_elasticsearch.Indexer directly to avoid exposing too much +} + +// NewIndexer creates a new elasticsearch indexer +func NewIndexer(url, indexerName string) *Indexer { + inner := inner_elasticsearch.NewIndexer(url, indexerName, issueIndexerLatestVersion, defaultMapping) + indexer := &Indexer{ + inner: inner, + Indexer: inner, + } + return indexer +} + +const ( + defaultMapping = `{ + "mappings": { + "properties": { + "id": { + "type": "integer", + "index": true + }, + "repo_id": { + "type": "integer", + "index": true + }, + "title": { + "type": "text", + "index": true + }, + "content": { + "type": "text", + "index": true + }, + "comments": { + "type" : "text", + "index": true + } + } + } + }` +) + +// Index will save the index data +func (b *Indexer) Index(ctx context.Context, issues []*internal.IndexerData) error { + if len(issues) == 0 { + return nil + } else if len(issues) == 1 { + issue := issues[0] + _, err := b.inner.Client.Index(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", issue.ID)). + BodyJson(map[string]interface{}{ + "id": issue.ID, + "repo_id": issue.RepoID, + "title": issue.Title, + "content": issue.Content, + "comments": issue.Comments, + }). + Do(ctx) + return err + } + + reqs := make([]elastic.BulkableRequest, 0) + for _, issue := range issues { + reqs = append(reqs, + elastic.NewBulkIndexRequest(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", issue.ID)). + Doc(map[string]interface{}{ + "id": issue.ID, + "repo_id": issue.RepoID, + "title": issue.Title, + "content": issue.Content, + "comments": issue.Comments, + }), + ) + } + + _, err := b.inner.Client.Bulk(). + Index(b.inner.VersionedIndexName()). + Add(reqs...). + Do(graceful.GetManager().HammerContext()) + return err +} + +// Delete deletes indexes by ids +func (b *Indexer) Delete(ctx context.Context, ids ...int64) error { + if len(ids) == 0 { + return nil + } else if len(ids) == 1 { + _, err := b.inner.Client.Delete(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", ids[0])). + Do(ctx) + return err + } + + reqs := make([]elastic.BulkableRequest, 0) + for _, id := range ids { + reqs = append(reqs, + elastic.NewBulkDeleteRequest(). + Index(b.inner.VersionedIndexName()). + Id(fmt.Sprintf("%d", id)), + ) + } + + _, err := b.inner.Client.Bulk(). + Index(b.inner.VersionedIndexName()). + Add(reqs...). + Do(graceful.GetManager().HammerContext()) + return err +} + +// Search searches for issues by given conditions. +// Returns the matching issue IDs +func (b *Indexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { + kwQuery := elastic.NewMultiMatchQuery(keyword, "title", "content", "comments") + query := elastic.NewBoolQuery() + query = query.Must(kwQuery) + if len(repoIDs) > 0 { + repoStrs := make([]interface{}, 0, len(repoIDs)) + for _, repoID := range repoIDs { + repoStrs = append(repoStrs, repoID) + } + repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) + query = query.Must(repoQuery) + } + searchResult, err := b.inner.Client.Search(). + Index(b.inner.VersionedIndexName()). + Query(query). + Sort("_score", false). + From(start).Size(limit). + Do(ctx) + if err != nil { + return nil, err + } + + hits := make([]internal.Match, 0, limit) + for _, hit := range searchResult.Hits.Hits { + id, _ := strconv.ParseInt(hit.Id, 10, 64) + hits = append(hits, internal.Match{ + ID: id, + }) + } + + return &internal.SearchResult{ + Total: searchResult.TotalHits(), + Hits: hits, + }, nil +} diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index f36ea1093541..9e2f13371e4a 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -5,16 +5,20 @@ package issues import ( "context" - "fmt" "os" "runtime/pprof" - "sync" + "sync/atomic" "time" - "code.gitea.io/gitea/models/db" + db_model "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/indexer/issues/bleve" + "code.gitea.io/gitea/modules/indexer/issues/db" + "code.gitea.io/gitea/modules/indexer/issues/elasticsearch" + "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/indexer/issues/meilisearch" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" @@ -22,81 +26,22 @@ import ( "code.gitea.io/gitea/modules/util" ) -// IndexerData data stored in the issue indexer -type IndexerData struct { - ID int64 `json:"id"` - RepoID int64 `json:"repo_id"` - Title string `json:"title"` - Content string `json:"content"` - Comments []string `json:"comments"` - IsDelete bool `json:"is_delete"` - IDs []int64 `json:"ids"` -} - -// Match represents on search result -type Match struct { - ID int64 `json:"id"` - Score float64 `json:"score"` -} - -// SearchResult represents search results -type SearchResult struct { - Total int64 - Hits []Match -} - -// Indexer defines an interface to indexer issues contents -type Indexer interface { - Init() (bool, error) - Ping() bool - Index(issue []*IndexerData) error - Delete(ids ...int64) error - Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) - Close() -} - -type indexerHolder struct { - indexer Indexer - mutex sync.RWMutex - cond *sync.Cond - cancelled bool -} - -func newIndexerHolder() *indexerHolder { - h := &indexerHolder{} - h.cond = sync.NewCond(h.mutex.RLocker()) - return h -} - -func (h *indexerHolder) cancel() { - h.mutex.Lock() - defer h.mutex.Unlock() - h.cancelled = true - h.cond.Broadcast() -} - -func (h *indexerHolder) set(indexer Indexer) { - h.mutex.Lock() - defer h.mutex.Unlock() - h.indexer = indexer - h.cond.Broadcast() -} - -func (h *indexerHolder) get() Indexer { - h.mutex.RLock() - defer h.mutex.RUnlock() - if h.indexer == nil && !h.cancelled { - h.cond.Wait() - } - return h.indexer -} - var ( // issueIndexerQueue queue of issue ids to be updated - issueIndexerQueue *queue.WorkerPoolQueue[*IndexerData] - holder = newIndexerHolder() + issueIndexerQueue *queue.WorkerPoolQueue[*internal.IndexerData] + // globalIndexer is the global indexer, it cannot be nil. + // When the real indexer is not ready, it will be a dummy indexer which will return error to explain it's not ready. + // So it's always safe use it as *globalIndexer.Load() and call its methods. + globalIndexer atomic.Pointer[internal.Indexer] + dummyIndexer *internal.Indexer ) +func init() { + i := internal.NewDummyIndexer() + dummyIndexer = &i + globalIndexer.Store(dummyIndexer) +} + // InitIssueIndexer initialize issue indexer, syncReindex is true then reindex until // all issue index done. func InitIssueIndexer(syncReindex bool) { @@ -107,33 +52,23 @@ func InitIssueIndexer(syncReindex bool) { // Create the Queue switch setting.Indexer.IssueType { case "bleve", "elasticsearch", "meilisearch": - handler := func(items ...*IndexerData) (unhandled []*IndexerData) { - indexer := holder.get() - if indexer == nil { - log.Warn("Issue indexer handler: indexer is not ready, retry later.") - return items - } - toIndex := make([]*IndexerData, 0, len(items)) + handler := func(items ...*internal.IndexerData) (unhandled []*internal.IndexerData) { + indexer := *globalIndexer.Load() + toIndex := make([]*internal.IndexerData, 0, len(items)) for _, indexerData := range items { log.Trace("IndexerData Process: %d %v %t", indexerData.ID, indexerData.IDs, indexerData.IsDelete) if indexerData.IsDelete { - if err := indexer.Delete(indexerData.IDs...); err != nil { + if err := indexer.Delete(ctx, indexerData.IDs...); err != nil { log.Error("Issue indexer handler: failed to from index: %v Error: %v", indexerData.IDs, err) - if !indexer.Ping() { - log.Error("Issue indexer handler: indexer is unavailable when deleting") - unhandled = append(unhandled, indexerData) - } + unhandled = append(unhandled, indexerData) } continue } toIndex = append(toIndex, indexerData) } - if err := indexer.Index(toIndex); err != nil { + if err := indexer.Index(ctx, toIndex); err != nil { log.Error("Error whilst indexing: %v Error: %v", toIndex, err) - if !indexer.Ping() { - log.Error("Issue indexer handler: indexer is unavailable when indexing") - unhandled = append(unhandled, toIndex...) - } + unhandled = append(unhandled, toIndex...) } return unhandled } @@ -144,7 +79,7 @@ func InitIssueIndexer(syncReindex bool) { log.Fatal("Unable to create issue indexer queue") } default: - issueIndexerQueue = queue.CreateSimpleQueue[*IndexerData](ctx, "issue_indexer", nil) + issueIndexerQueue = queue.CreateSimpleQueue[*internal.IndexerData](ctx, "issue_indexer", nil) } graceful.GetManager().RunAtTerminate(finished) @@ -154,7 +89,11 @@ func InitIssueIndexer(syncReindex bool) { pprof.SetGoroutineLabels(ctx) start := time.Now() log.Info("PID %d: Initializing Issue Indexer: %s", os.Getpid(), setting.Indexer.IssueType) - var populate bool + var ( + issueIndexer internal.Indexer + existed bool + err error + ) switch setting.Indexer.IssueType { case "bleve": defer func() { @@ -162,62 +101,45 @@ func InitIssueIndexer(syncReindex bool) { log.Error("PANIC whilst initializing issue indexer: %v\nStacktrace: %s", err, log.Stack(2)) log.Error("The indexer files are likely corrupted and may need to be deleted") log.Error("You can completely remove the %q directory to make Gitea recreate the indexes", setting.Indexer.IssuePath) - holder.cancel() + globalIndexer.Store(dummyIndexer) log.Fatal("PID: %d Unable to initialize the Bleve Issue Indexer at path: %s Error: %v", os.Getpid(), setting.Indexer.IssuePath, err) } }() - issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath) - exist, err := issueIndexer.Init() + issueIndexer = bleve.NewIndexer(setting.Indexer.IssuePath) + existed, err = issueIndexer.Init(ctx) if err != nil { - holder.cancel() log.Fatal("Unable to initialize Bleve Issue Indexer at path: %s Error: %v", setting.Indexer.IssuePath, err) } - populate = !exist - holder.set(issueIndexer) - graceful.GetManager().RunAtTerminate(func() { - log.Debug("Closing issue indexer") - issueIndexer := holder.get() - if issueIndexer != nil { - issueIndexer.Close() - } - log.Info("PID: %d Issue Indexer closed", os.Getpid()) - }) - log.Debug("Created Bleve Indexer") case "elasticsearch": - issueIndexer, err := NewElasticSearchIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueIndexerName) - if err != nil { - log.Fatal("Unable to initialize Elastic Search Issue Indexer at connection: %s Error: %v", setting.Indexer.IssueConnStr, err) - } - exist, err := issueIndexer.Init() + issueIndexer = elasticsearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueIndexerName) + existed, err = issueIndexer.Init(ctx) if err != nil { log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err) } - populate = !exist - holder.set(issueIndexer) case "db": - issueIndexer := &DBIndexer{} - holder.set(issueIndexer) + issueIndexer = db.NewIndexer() case "meilisearch": - issueIndexer, err := NewMeilisearchIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName) - if err != nil { - log.Fatal("Unable to initialize Meilisearch Issue Indexer at connection: %s Error: %v", setting.Indexer.IssueConnStr, err) - } - exist, err := issueIndexer.Init() + issueIndexer = meilisearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName) + existed, err = issueIndexer.Init(ctx) if err != nil { log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err) } - populate = !exist - holder.set(issueIndexer) default: - holder.cancel() log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) } + globalIndexer.Store(&issueIndexer) + + graceful.GetManager().RunAtTerminate(func() { + log.Debug("Closing issue indexer") + (*globalIndexer.Load()).Close() + log.Info("PID: %d Issue Indexer closed", os.Getpid()) + }) // Start processing the queue go graceful.GetManager().RunWithCancel(issueIndexerQueue) // Populate the index - if populate { + if !existed { if syncReindex { graceful.GetManager().RunWithShutdownContext(populateIssueIndexer) } else { @@ -266,8 +188,8 @@ func populateIssueIndexer(ctx context.Context) { default: } repos, _, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{ - ListOptions: db.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, - OrderBy: db.SearchOrderByID, + ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, + OrderBy: db_model.SearchOrderByID, Private: true, Collaborate: util.OptionalBoolFalse, }) @@ -320,7 +242,7 @@ func UpdateIssueIndexer(issue *issues_model.Issue) { comments = append(comments, comment.Content) } } - indexerData := &IndexerData{ + indexerData := &internal.IndexerData{ ID: issue.ID, RepoID: issue.RepoID, Title: issue.Title, @@ -345,7 +267,7 @@ func DeleteRepoIssueIndexer(ctx context.Context, repo *repo_model.Repository) { if len(ids) == 0 { return } - indexerData := &IndexerData{ + indexerData := &internal.IndexerData{ IDs: ids, IsDelete: true, } @@ -358,12 +280,7 @@ func DeleteRepoIssueIndexer(ctx context.Context, repo *repo_model.Repository) { // WARNNING: You have to ensure user have permission to visit repoIDs' issues func SearchIssuesByKeyword(ctx context.Context, repoIDs []int64, keyword string) ([]int64, error) { var issueIDs []int64 - indexer := holder.get() - - if indexer == nil { - log.Error("SearchIssuesByKeyword(): unable to get indexer!") - return nil, fmt.Errorf("unable to get issue indexer") - } + indexer := *globalIndexer.Load() res, err := indexer.Search(ctx, keyword, repoIDs, 50, 0) if err != nil { return nil, err @@ -375,12 +292,6 @@ func SearchIssuesByKeyword(ctx context.Context, repoIDs []int64, keyword string) } // IsAvailable checks if issue indexer is available -func IsAvailable() bool { - indexer := holder.get() - if indexer == nil { - log.Error("IsAvailable(): unable to get indexer!") - return false - } - - return indexer.Ping() +func IsAvailable(ctx context.Context) bool { + return (*globalIndexer.Load()).Ping(ctx) == nil } diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index a2d1794f4b66..5962a4ee9cb7 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/indexer/issues/bleve" "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models" @@ -42,8 +43,7 @@ func TestBleveSearchIssues(t *testing.T) { setting.LoadQueueSettings() InitIssueIndexer(true) defer func() { - indexer := holder.get() - if bleveIndexer, ok := indexer.(*BleveIndexer); ok { + if bleveIndexer, ok := (*globalIndexer.Load()).(*bleve.Indexer); ok { bleveIndexer.Close() } }() diff --git a/modules/indexer/issues/internal/indexer.go b/modules/indexer/issues/internal/indexer.go new file mode 100644 index 000000000000..553c8a573cdc --- /dev/null +++ b/modules/indexer/issues/internal/indexer.go @@ -0,0 +1,42 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/modules/indexer/internal" +) + +// Indexer defines an interface to indexer issues contents +type Indexer interface { + internal.Indexer + Index(ctx context.Context, issue []*IndexerData) error + Delete(ctx context.Context, ids ...int64) error + Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) +} + +// NewDummyIndexer returns a dummy indexer +func NewDummyIndexer() Indexer { + return &dummyIndexer{ + Indexer: internal.NewDummyIndexer(), + } +} + +type dummyIndexer struct { + internal.Indexer +} + +func (d *dummyIndexer) Index(ctx context.Context, issue []*IndexerData) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Delete(ctx context.Context, ids ...int64) error { + return fmt.Errorf("indexer is not ready") +} + +func (d *dummyIndexer) Search(ctx context.Context, kw string, repoIDs []int64, limit, start int) (*SearchResult, error) { + return nil, fmt.Errorf("indexer is not ready") +} diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go new file mode 100644 index 000000000000..8c206fc1cfcd --- /dev/null +++ b/modules/indexer/issues/internal/model.go @@ -0,0 +1,27 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package internal + +// IndexerData data stored in the issue indexer +type IndexerData struct { + ID int64 `json:"id"` + RepoID int64 `json:"repo_id"` + Title string `json:"title"` + Content string `json:"content"` + Comments []string `json:"comments"` + IsDelete bool `json:"is_delete"` + IDs []int64 `json:"ids"` +} + +// Match represents on search result +type Match struct { + ID int64 `json:"id"` + Score float64 `json:"score"` +} + +// SearchResult represents search results +type SearchResult struct { + Total int64 + Hits []Match +} diff --git a/modules/indexer/issues/meilisearch.go b/modules/indexer/issues/meilisearch.go deleted file mode 100644 index 990bc57a05f5..000000000000 --- a/modules/indexer/issues/meilisearch.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package issues - -import ( - "context" - "strconv" - "strings" - "sync" - "time" - - "github.com/meilisearch/meilisearch-go" -) - -var _ Indexer = &MeilisearchIndexer{} - -// MeilisearchIndexer implements Indexer interface -type MeilisearchIndexer struct { - client *meilisearch.Client - indexerName string - available bool - stopTimer chan struct{} - lock sync.RWMutex -} - -// MeilisearchIndexer creates a new meilisearch indexer -func NewMeilisearchIndexer(url, apiKey, indexerName string) (*MeilisearchIndexer, error) { - client := meilisearch.NewClient(meilisearch.ClientConfig{ - Host: url, - APIKey: apiKey, - }) - - indexer := &MeilisearchIndexer{ - client: client, - indexerName: indexerName, - available: true, - stopTimer: make(chan struct{}), - } - - ticker := time.NewTicker(10 * time.Second) - go func() { - for { - select { - case <-ticker.C: - indexer.checkAvailability() - case <-indexer.stopTimer: - ticker.Stop() - return - } - } - }() - - return indexer, nil -} - -// Init will initialize the indexer -func (b *MeilisearchIndexer) Init() (bool, error) { - _, err := b.client.GetIndex(b.indexerName) - if err == nil { - return true, nil - } - _, err = b.client.CreateIndex(&meilisearch.IndexConfig{ - Uid: b.indexerName, - PrimaryKey: "id", - }) - if err != nil { - return false, b.checkError(err) - } - - _, err = b.client.Index(b.indexerName).UpdateFilterableAttributes(&[]string{"repo_id"}) - return false, b.checkError(err) -} - -// Ping checks if meilisearch is available -func (b *MeilisearchIndexer) Ping() bool { - b.lock.RLock() - defer b.lock.RUnlock() - return b.available -} - -// Index will save the index data -func (b *MeilisearchIndexer) Index(issues []*IndexerData) error { - if len(issues) == 0 { - return nil - } - for _, issue := range issues { - _, err := b.client.Index(b.indexerName).AddDocuments(issue) - if err != nil { - return b.checkError(err) - } - } - // TODO: bulk send index data - return nil -} - -// Delete deletes indexes by ids -func (b *MeilisearchIndexer) Delete(ids ...int64) error { - if len(ids) == 0 { - return nil - } - - for _, id := range ids { - _, err := b.client.Index(b.indexerName).DeleteDocument(strconv.FormatInt(id, 10)) - if err != nil { - return b.checkError(err) - } - } - // TODO: bulk send deletes - return nil -} - -// Search searches for issues by given conditions. -// Returns the matching issue IDs -func (b *MeilisearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { - repoFilters := make([]string, 0, len(repoIDs)) - for _, repoID := range repoIDs { - repoFilters = append(repoFilters, "repo_id = "+strconv.FormatInt(repoID, 10)) - } - filter := strings.Join(repoFilters, " OR ") - searchRes, err := b.client.Index(b.indexerName).Search(keyword, &meilisearch.SearchRequest{ - Filter: filter, - Limit: int64(limit), - Offset: int64(start), - }) - if err != nil { - return nil, b.checkError(err) - } - - hits := make([]Match, 0, len(searchRes.Hits)) - for _, hit := range searchRes.Hits { - hits = append(hits, Match{ - ID: int64(hit.(map[string]interface{})["id"].(float64)), - }) - } - return &SearchResult{ - Total: searchRes.TotalHits, - Hits: hits, - }, nil -} - -// Close implements indexer -func (b *MeilisearchIndexer) Close() { - select { - case <-b.stopTimer: - default: - close(b.stopTimer) - } -} - -func (b *MeilisearchIndexer) checkError(err error) error { - return err -} - -func (b *MeilisearchIndexer) checkAvailability() { - _, err := b.client.Health() - if err != nil { - b.setAvailability(false) - return - } - b.setAvailability(true) -} - -func (b *MeilisearchIndexer) setAvailability(available bool) { - b.lock.Lock() - defer b.lock.Unlock() - - if b.available == available { - return - } - - b.available = available -} diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go new file mode 100644 index 000000000000..877c04f1dcb2 --- /dev/null +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -0,0 +1,98 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package meilisearch + +import ( + "context" + "strconv" + "strings" + + indexer_internal "code.gitea.io/gitea/modules/indexer/internal" + inner_meilisearch "code.gitea.io/gitea/modules/indexer/internal/meilisearch" + "code.gitea.io/gitea/modules/indexer/issues/internal" + + "github.com/meilisearch/meilisearch-go" +) + +const ( + issueIndexerLatestVersion = 0 +) + +var _ internal.Indexer = &Indexer{} + +// Indexer implements Indexer interface +type Indexer struct { + inner *inner_meilisearch.Indexer + indexer_internal.Indexer // do not composite inner_meilisearch.Indexer directly to avoid exposing too much +} + +// NewIndexer creates a new meilisearch indexer +func NewIndexer(url, apiKey, indexerName string) *Indexer { + inner := inner_meilisearch.NewIndexer(url, apiKey, indexerName, issueIndexerLatestVersion) + indexer := &Indexer{ + inner: inner, + Indexer: inner, + } + return indexer +} + +// Index will save the index data +func (b *Indexer) Index(_ context.Context, issues []*internal.IndexerData) error { + if len(issues) == 0 { + return nil + } + for _, issue := range issues { + _, err := b.inner.Client.Index(b.inner.VersionedIndexName()).AddDocuments(issue) + if err != nil { + return err + } + } + // TODO: bulk send index data + return nil +} + +// Delete deletes indexes by ids +func (b *Indexer) Delete(_ context.Context, ids ...int64) error { + if len(ids) == 0 { + return nil + } + + for _, id := range ids { + _, err := b.inner.Client.Index(b.inner.VersionedIndexName()).DeleteDocument(strconv.FormatInt(id, 10)) + if err != nil { + return err + } + } + // TODO: bulk send deletes + return nil +} + +// Search searches for issues by given conditions. +// Returns the matching issue IDs +func (b *Indexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*internal.SearchResult, error) { + repoFilters := make([]string, 0, len(repoIDs)) + for _, repoID := range repoIDs { + repoFilters = append(repoFilters, "repo_id = "+strconv.FormatInt(repoID, 10)) + } + filter := strings.Join(repoFilters, " OR ") + searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{ + Filter: filter, + Limit: int64(limit), + Offset: int64(start), + }) + if err != nil { + return nil, err + } + + hits := make([]internal.Match, 0, len(searchRes.Hits)) + for _, hit := range searchRes.Hits { + hits = append(hits, internal.Match{ + ID: int64(hit.(map[string]interface{})["id"].(float64)), + }) + } + return &internal.SearchResult{ + Total: searchRes.TotalHits, + Hits: hits, + }, nil +} diff --git a/modules/indexer/stats/indexer.go b/modules/indexer/stats/indexer.go index 1c01e25e29e2..6bfa8bdedb97 100644 --- a/modules/indexer/stats/indexer.go +++ b/modules/indexer/stats/indexer.go @@ -11,6 +11,7 @@ import ( ) // Indexer defines an interface to index repository stats +// TODO: this indexer is quite different from the others, maybe this package should be moved out from module/indexer type Indexer interface { Index(id int64) error Close() diff --git a/modules/log/logger_global.go b/modules/log/logger_global.go index 5ccef34b5b75..994acfedbb3a 100644 --- a/modules/log/logger_global.go +++ b/modules/log/logger_global.go @@ -79,5 +79,5 @@ func SetConsoleLogger(loggerName, writerName string, level Level) { Colorize: CanColorStdout, WriterOption: WriterConsoleOption{}, }) - GetManager().GetLogger(loggerName).RemoveAllWriters().AddWriters(writer) + GetManager().GetLogger(loggerName).ReplaceAllWriters(writer) } diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go index 903d8cefc2a5..d38c6516ed5c 100644 --- a/modules/log/logger_impl.go +++ b/modules/log/logger_impl.go @@ -96,7 +96,10 @@ func (l *LoggerImpl) removeWriterInternal(w EventWriter) { func (l *LoggerImpl) AddWriters(writer ...EventWriter) { l.eventWriterMu.Lock() defer l.eventWriterMu.Unlock() + l.addWritersInternal(writer...) +} +func (l *LoggerImpl) addWritersInternal(writer ...EventWriter) { for _, w := range writer { if old, ok := l.eventWriters[w.GetWriterName()]; ok { l.removeWriterInternal(old) @@ -126,8 +129,8 @@ func (l *LoggerImpl) RemoveWriter(modeName string) error { return nil } -// RemoveAllWriters removes all writers from the logger, non-shared writers are closed and flushed -func (l *LoggerImpl) RemoveAllWriters() *LoggerImpl { +// ReplaceAllWriters replaces all writers from the logger, non-shared writers are closed and flushed +func (l *LoggerImpl) ReplaceAllWriters(writer ...EventWriter) { l.eventWriterMu.Lock() defer l.eventWriterMu.Unlock() @@ -135,8 +138,7 @@ func (l *LoggerImpl) RemoveAllWriters() *LoggerImpl { l.removeWriterInternal(w) } l.eventWriters = map[string]EventWriter{} - l.syncLevelInternal() - return l + l.addWritersInternal(writer...) } // DumpWriters dumps the writers as a JSON map, it's used for debugging and display purposes. @@ -161,7 +163,7 @@ func (l *LoggerImpl) DumpWriters() map[string]any { // Close closes the logger, non-shared writers are closed and flushed func (l *LoggerImpl) Close() { - l.RemoveAllWriters() + l.ReplaceAllWriters() l.ctxCancel() } @@ -233,7 +235,6 @@ func NewLoggerWithWriters(ctx context.Context, name string, writer ...EventWrite l.ctx, l.ctxCancel = newProcessTypedContext(ctx, "Logger: "+name) l.LevelLogger = BaseLoggerToGeneralLogger(l) l.eventWriters = map[string]EventWriter{} - l.syncLevelInternal() l.AddWriters(writer...) return l } diff --git a/modules/log/manager_test.go b/modules/log/manager_test.go index aa01f79980c5..b8fbf846133c 100644 --- a/modules/log/manager_test.go +++ b/modules/log/manager_test.go @@ -23,7 +23,7 @@ func TestSharedWorker(t *testing.T) { loggerTest := m.GetLogger("test") loggerTest.AddWriters(w) loggerTest.Info("msg-1") - loggerTest.RemoveAllWriters() // the shared writer is not closed here + loggerTest.ReplaceAllWriters() // the shared writer is not closed here loggerTest.Info("never seen") // the shared writer can still be used later diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index e1762bb1ee9a..be4d774a8b05 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -17,6 +17,7 @@ import ( // Notifier defines an interface to notify receiver type Notifier interface { Run() + NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) NotifyMigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) NotifyDeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 338790b35642..56a25394f995 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -145,6 +145,10 @@ func (*NullNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *user_mod func (*NullNotifier) NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { } +// NotifyAdoptRepository places a place holder function +func (*NullNotifier) NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { +} + // NotifyDeleteRepository places a place holder function func (*NullNotifier) NotifyDeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) { } diff --git a/modules/notification/indexer/indexer.go b/modules/notification/indexer/indexer.go index 0661c2c1ab49..bb652e39426b 100644 --- a/modules/notification/indexer/indexer.go +++ b/modules/notification/indexer/indexer.go @@ -29,6 +29,10 @@ func NewNotifier() base.Notifier { return &indexerNotifier{} } +func (r *indexerNotifier) NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { + r.NotifyMigrateRepository(ctx, doer, u, repo) +} + func (r *indexerNotifier) NotifyCreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 6153c9e3d687..99e1a06ebda4 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -274,6 +274,13 @@ func NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo } } +// NotifyAdoptRepository notifies the adoption of a repository to notifiers +func NotifyAdoptRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { + for _, notifier := range notifiers { + notifier.NotifyAdoptRepository(ctx, doer, u, repo) + } +} + // NotifyMigrateRepository notifies create repository to notifiers func NotifyMigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { for _, notifier := range notifiers { diff --git a/modules/repository/branch.go b/modules/repository/branch.go new file mode 100644 index 000000000000..bffadd62f4d9 --- /dev/null +++ b/modules/repository/branch.go @@ -0,0 +1,135 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" +) + +// SyncRepoBranches synchronizes branch table with repository branches +func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return 0, err + } + + log.Debug("SyncRepoBranches: in Repo[%d:%s]", repo.ID, repo.FullName()) + + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + log.Error("OpenRepository[%s]: %w", repo.RepoPath(), err) + return 0, err + } + defer gitRepo.Close() + + return SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID) +} + +func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) { + allBranches := container.Set[string]{} + { + branches, _, err := gitRepo.GetBranchNames(0, 0) + if err != nil { + return 0, err + } + log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches) + for _, branch := range branches { + allBranches.Add(branch) + } + } + + dbBranches := make(map[string]*git_model.Branch) + { + branches, err := git_model.FindBranches(ctx, git_model.FindBranchOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + RepoID: repo.ID, + }) + if err != nil { + return 0, err + } + for _, branch := range branches { + dbBranches[branch.Name] = branch + } + } + + var toAdd []*git_model.Branch + var toUpdate []*git_model.Branch + var toRemove []int64 + for branch := range allBranches { + dbb := dbBranches[branch] + commit, err := gitRepo.GetBranchCommit(branch) + if err != nil { + return 0, err + } + if dbb == nil { + toAdd = append(toAdd, &git_model.Branch{ + RepoID: repo.ID, + Name: branch, + CommitID: commit.ID.String(), + CommitMessage: commit.Summary(), + PusherID: doerID, + CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), + }) + } else if commit.ID.String() != dbb.CommitID { + toUpdate = append(toUpdate, &git_model.Branch{ + ID: dbb.ID, + RepoID: repo.ID, + Name: branch, + CommitID: commit.ID.String(), + CommitMessage: commit.Summary(), + PusherID: doerID, + CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), + }) + } + } + + for _, dbBranch := range dbBranches { + if !allBranches.Contains(dbBranch.Name) && !dbBranch.IsDeleted { + toRemove = append(toRemove, dbBranch.ID) + } + } + + log.Trace("SyncRepoBranches[%s]: toAdd: %v, toUpdate: %v, toRemove: %v", repo.FullName(), toAdd, toUpdate, toRemove) + + if len(toAdd) == 0 && len(toRemove) == 0 && len(toUpdate) == 0 { + return int64(len(allBranches)), nil + } + + if err := db.WithTx(ctx, func(subCtx context.Context) error { + if len(toAdd) > 0 { + if err := git_model.AddBranches(subCtx, toAdd); err != nil { + return err + } + } + + for _, b := range toUpdate { + if _, err := db.GetEngine(subCtx).ID(b.ID). + Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted"). + Update(b); err != nil { + return err + } + } + + if len(toRemove) > 0 { + if err := git_model.DeleteBranches(subCtx, repo.ID, doerID, toRemove); err != nil { + return err + } + } + + return nil + }); err != nil { + return 0, err + } + return int64(len(allBranches)), nil +} diff --git a/modules/repository/create.go b/modules/repository/create.go index 0558d7f1c006..e8a1b8ba2bf8 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -330,7 +330,7 @@ func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error { return fmt.Errorf("updateSize: GetLFSMetaObjects: %w", err) } - return repo_model.UpdateRepoSize(ctx, repo.ID, size+lfsSize) + return repo_model.UpdateRepoSize(ctx, repo.ID, size, lfsSize) } // CheckDaemonExportOK creates/removes git-daemon-export-ok for git-daemon... diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 102c5af1c91f..cb25daa10b33 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -372,12 +372,12 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ return generateRepo, nil } +var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`) + // Sanitize user input to valid OS filenames // // Based on https://github.com/sindresorhus/filename-reserved-regex // Adds ".." to prevent directory traversal func fileNameSanitize(s string) string { - re := regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`) - - return strings.TrimSpace(re.ReplaceAllString(s, "_")) + return strings.TrimSpace(fileNameSanitizeRegexp.ReplaceAllString(s, "_")) } diff --git a/modules/repository/init.go b/modules/repository/init.go index f079f72b7711..84648f45ebf4 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -351,6 +351,12 @@ func initRepository(ctx context.Context, repoPath string, u *user_model.User, re if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } + + if !repo.IsEmpty { + if _, err := SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { + return fmt.Errorf("SyncRepoBranches: %w", err) + } + } } if err = UpdateRepository(ctx, repo, false); err != nil { diff --git a/modules/repository/repo.go b/modules/repository/repo.go index bcb43f15e1d1..6a11315cc404 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -151,6 +151,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } } + if _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil { + return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) + } + if !opts.Releases { // note: this will greatly improve release (tag) sync // for pull-mirrors with many tags @@ -169,7 +173,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } } - ctx, committer, err := db.TxContext(db.DefaultContext) + ctx, committer, err := db.TxContext(ctx) if err != nil { return nil, err } diff --git a/modules/setting/actions.go b/modules/setting/actions.go index 1c8075cd6cc5..a13330dcd18a 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -5,6 +5,9 @@ package setting import ( "fmt" + "strings" + + "code.gitea.io/gitea/modules/log" ) // Actions settings @@ -13,13 +16,36 @@ var ( LogStorage *Storage // how the created logs should be stored ArtifactStorage *Storage // how the created artifacts should be stored Enabled bool - DefaultActionsURL string `ini:"DEFAULT_ACTIONS_URL"` + DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` }{ Enabled: false, - DefaultActionsURL: "https://gitea.com", + DefaultActionsURL: defaultActionsURLGitHub, } ) +type defaultActionsURL string + +func (url defaultActionsURL) URL() string { + switch url { + case defaultActionsURLGitHub: + return "https://github.com" + case defaultActionsURLSelf: + return strings.TrimSuffix(AppURL, "/") + default: + // This should never happen, but just in case, use GitHub as fallback + return "https://github.com" + } +} + +const ( + defaultActionsURLGitHub = "github" // https://github.com + defaultActionsURLSelf = "self" // the root URL of the self-hosted Gitea instance + // DefaultActionsURL only supports GitHub and the self-hosted Gitea. + // It's intentionally not supported more, so please be cautious before adding more like "gitea" or "gitlab". + // If you get some trouble with `uses: username/action_name@version` in your workflow, + // please consider to use `uses: https://the_url_you_want_to_use/username/action_name@version` instead. +) + func loadActionsFrom(rootCfg ConfigProvider) error { sec := rootCfg.Section("actions") err := sec.MapTo(&Actions) @@ -27,6 +53,19 @@ func loadActionsFrom(rootCfg ConfigProvider) error { return fmt.Errorf("failed to map Actions settings: %v", err) } + if urls := string(Actions.DefaultActionsURL); urls != defaultActionsURLGitHub && urls != defaultActionsURLSelf { + url := strings.Split(urls, ",")[0] + if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") { + log.Error("[actions] DEFAULT_ACTIONS_URL does not support %q as custom URL any longer, fallback to %q", + urls, + defaultActionsURLGitHub, + ) + Actions.DefaultActionsURL = defaultActionsURLGitHub + } else { + return fmt.Errorf("unsupported [actions] DEFAULT_ACTIONS_URL: %q", urls) + } + } + // don't support to read configuration from [actions] Actions.LogStorage, err = getStorage(rootCfg, "actions_log", "", nil) if err != nil { diff --git a/modules/setting/actions_test.go b/modules/setting/actions_test.go index a1cc8fe333b0..3645a3f5dadc 100644 --- a/modules/setting/actions_test.go +++ b/modules/setting/actions_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_getStorageInheritNameSectionTypeForActions(t *testing.T) { @@ -95,3 +96,86 @@ STORAGE_TYPE = minio assert.EqualValues(t, "local", Actions.ArtifactStorage.Type) assert.EqualValues(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path)) } + +func Test_getDefaultActionsURLForActions(t *testing.T) { + oldActions := Actions + oldAppURL := AppURL + defer func() { + Actions = oldActions + AppURL = oldAppURL + }() + + AppURL = "http://test_get_default_actions_url_for_actions:3000/" + + tests := []struct { + name string + iniStr string + wantErr assert.ErrorAssertionFunc + wantURL string + }{ + { + name: "default", + iniStr: ` +[actions] +`, + wantErr: assert.NoError, + wantURL: "https://github.com", + }, + { + name: "github", + iniStr: ` +[actions] +DEFAULT_ACTIONS_URL = github +`, + wantErr: assert.NoError, + wantURL: "https://github.com", + }, + { + name: "self", + iniStr: ` +[actions] +DEFAULT_ACTIONS_URL = self +`, + wantErr: assert.NoError, + wantURL: "http://test_get_default_actions_url_for_actions:3000", + }, + { + name: "custom url", + iniStr: ` +[actions] +DEFAULT_ACTIONS_URL = https://gitea.com +`, + wantErr: assert.NoError, + wantURL: "https://github.com", + }, + { + name: "custom urls", + iniStr: ` +[actions] +DEFAULT_ACTIONS_URL = https://gitea.com,https://github.com +`, + wantErr: assert.NoError, + wantURL: "https://github.com", + }, + { + name: "invalid", + iniStr: ` +[actions] +DEFAULT_ACTIONS_URL = gitea +`, + wantErr: assert.Error, + wantURL: "https://github.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := NewConfigProviderFromData(tt.iniStr) + require.NoError(t, err) + if !tt.wantErr(t, loadActionsFrom(cfg)) { + return + } + assert.EqualValues(t, tt.wantURL, Actions.DefaultActionsURL.URL()) + }) + } +} diff --git a/modules/setting/database.go b/modules/setting/database.go index 7a7c7029a430..709655368c67 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -12,8 +12,6 @@ import ( "path/filepath" "strings" "time" - - "code.gitea.io/gitea/modules/log" ) var ( @@ -36,7 +34,7 @@ var ( SSLMode string Path string LogSQL bool - Charset string + MysqlCharset string Timeout int // seconds SQLiteJournalMode string DBConnectRetries int @@ -60,11 +58,6 @@ func LoadDBSetting() { func loadDBSetting(rootCfg ConfigProvider) { sec := rootCfg.Section("database") Database.Type = DatabaseType(sec.Key("DB_TYPE").String()) - defaultCharset := "utf8" - - if Database.Type.IsMySQL() { - defaultCharset = "utf8mb4" - } Database.Host = sec.Key("HOST").String() Database.Name = sec.Key("NAME").String() @@ -74,10 +67,7 @@ func loadDBSetting(rootCfg ConfigProvider) { } Database.Schema = sec.Key("SCHEMA").String() Database.SSLMode = sec.Key("SSL_MODE").MustString("disable") - Database.Charset = sec.Key("CHARSET").In(defaultCharset, []string{"utf8", "utf8mb4"}) - if Database.Type.IsMySQL() && defaultCharset != "utf8mb4" { - log.Error("Deprecated database mysql charset utf8 support, please use utf8mb4 or convert utf8 to utf8mb4.") - } + Database.MysqlCharset = sec.Key("MYSQL_CHARSET").MustString("utf8mb4") // do not document it, end users won't need it. Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) @@ -101,9 +91,9 @@ func loadDBSetting(rootCfg ConfigProvider) { // DBConnStr returns database connection string func DBConnStr() (string, error) { var connStr string - Param := "?" - if strings.Contains(Database.Name, Param) { - Param = "&" + paramSep := "?" + if strings.Contains(Database.Name, paramSep) { + paramSep = "&" } switch Database.Type { case "mysql": @@ -116,15 +106,15 @@ func DBConnStr() (string, error) { tls = "false" } connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=%s&parseTime=true&tls=%s", - Database.User, Database.Passwd, connType, Database.Host, Database.Name, Param, Database.Charset, tls) + Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, Database.MysqlCharset, tls) case "postgres": - connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, Param, Database.SSLMode) + connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, paramSep, Database.SSLMode) case "mssql": host, port := ParseMSSQLHostPort(Database.Host) connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, Database.Name, Database.User, Database.Passwd) case "sqlite3": if !EnableSQLite3 { - return "", errors.New("this binary version does not build support for SQLite3") + return "", errors.New("this Gitea binary was not built with SQLite3 support") } if err := os.MkdirAll(path.Dir(Database.Path), os.ModePerm); err != nil { return "", fmt.Errorf("Failed to create directories: %w", err) @@ -136,7 +126,7 @@ func DBConnStr() (string, error) { connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate%s", Database.Path, Database.Timeout, journalMode) default: - return "", fmt.Errorf("Unknown database type: %s", Database.Type) + return "", fmt.Errorf("unknown database type: %s", Database.Type) } return connStr, nil diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 140a96f9eda8..784a99582d4c 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -53,6 +53,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error { return nil } + LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("lfs"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") + LFS.JWTSecretBytes = make([]byte, 32) n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64)) diff --git a/modules/setting/log.go b/modules/setting/log.go index af64ea8d8537..66206f8f4b22 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -244,7 +244,7 @@ func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, logger eventWriters = append(eventWriters, eventWriter) } - manager.GetLogger(loggerName).RemoveAllWriters().AddWriters(eventWriters...) + manager.GetLogger(loggerName).ReplaceAllWriters(eventWriters...) } func InitSQLLoggersForCli(level log.Level) { diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 83c607a416a9..78a9462de9a6 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -116,6 +116,12 @@ func loadOAuth2From(rootCfg ConfigProvider) { return } + if !OAuth2.Enable { + return + } + + OAuth2.JWTSecretBase64 = loadSecret(rootCfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET") + if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) { OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile) } diff --git a/modules/setting/path.go b/modules/setting/path.go index 91bb2e9bb777..163f1d159067 100644 --- a/modules/setting/path.go +++ b/modules/setting/path.go @@ -89,6 +89,12 @@ func (s *stringWithDefault) Set(v string) { // InitWorkPathAndCommonConfig will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf and load common settings, func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) { + InitWorkPathAndCfgProvider(getEnvFn, args) + LoadCommonSettings() +} + +// InitWorkPathAndCfgProvider will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf +func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) { tryAbsPath := func(paths ...string) string { s := paths[len(paths)-1] for i := len(paths) - 2; i >= 0; i-- { @@ -186,6 +192,4 @@ func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWork AppWorkPath = tmpWorkPath.Value CustomPath = tmpCustomPath.Value CustomConf = tmpCustomConf.Value - - LoadCommonSettings() } diff --git a/modules/setting/security.go b/modules/setting/security.go index c39eb7f3ebd6..5f1f9f4ade89 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -76,7 +76,7 @@ func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string { // only file URIs are allowed default: - log.Fatal("Unsupported URI-Scheme %q (INTERNAL_TOKEN_URI = %q)", tempURI.Scheme, uri) + log.Fatal("Unsupported URI-Scheme %q (%q = %q)", tempURI.Scheme, uriKey, uri) return "" } } diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index a5a9da0b3676..bbb7f5ab6cda 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -173,7 +173,7 @@ func loadSSHFrom(rootCfg ConfigProvider) { } } - SSH.AuthorizedKeysBackup = sec.Key("SSH_AUTHORIZED_KEYS_BACKUP").MustBool(true) + SSH.AuthorizedKeysBackup = sec.Key("SSH_AUTHORIZED_KEYS_BACKUP").MustBool(false) SSH.CreateAuthorizedKeysFile = sec.Key("SSH_CREATE_AUTHORIZED_KEYS_FILE").MustBool(true) SSH.AuthorizedPrincipalsBackup = false diff --git a/modules/structs/repo.go b/modules/structs/repo.go index fc4ed03de553..94992de72ebc 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -10,9 +10,9 @@ import ( // Permission represents a set of permissions type Permission struct { - Admin bool `json:"admin"` - Push bool `json:"push"` - Pull bool `json:"pull"` + Admin bool `json:"admin"` // Admin indicates if the user is an administrator of the repository. + Push bool `json:"push"` // Push indicates if the user can push code to the repository. + Pull bool `json:"pull"` // Pull indicates if the user can pull code from the repository. } // InternalTracker represents settings for internal tracker @@ -380,3 +380,9 @@ type NewIssuePinsAllowed struct { Issues bool `json:"issues"` PullRequests bool `json:"pull_requests"` } + +// UpdateRepoAvatarUserOption options when updating the repo avatar +type UpdateRepoAvatarOption struct { + // image must be base64 encoded + Image string `json:"image" binding:"Required"` +} diff --git a/modules/structs/user.go b/modules/structs/user.go index f68b92ac069e..0df67894b039 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -102,3 +102,9 @@ type RenameUserOption struct { // unique: true NewName string `json:"new_username" binding:"Required"` } + +// UpdateUserAvatarUserOption options when updating the user avatar +type UpdateUserAvatarOption struct { + // image must be base64 encoded + Image string `json:"image" binding:"Required"` +} diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index a26c0531f81d..d23103ce1bc5 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -81,16 +81,16 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa // RenderCommitBody extracts the body of a commit message without its title. func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML { - msgLine := strings.TrimRightFunc(msg, unicode.IsSpace) + msgLine := strings.TrimSpace(msg) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { msgLine = msgLine[lineEnd+1:] } else { - return template.HTML("") + return "" } msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace) if len(msgLine) == 0 { - return template.HTML("") + return "" } renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go new file mode 100644 index 000000000000..29d3ed3a56c4 --- /dev/null +++ b/modules/templates/util_render_test.go @@ -0,0 +1,56 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package templates + +import ( + "context" + "html/template" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderCommitBody(t *testing.T) { + type args struct { + ctx context.Context + msg string + urlPrefix string + metas map[string]string + } + tests := []struct { + name string + args args + want template.HTML + }{ + { + name: "multiple lines", + args: args{ + ctx: context.Background(), + msg: "first line\nsecond line", + }, + want: "second line", + }, + { + name: "multiple lines with leading newlines", + args: args{ + ctx: context.Background(), + msg: "\n\n\n\nfirst line\nsecond line", + }, + want: "second line", + }, + { + name: "multiple lines with trailing newlines", + args: args{ + ctx: context.Background(), + msg: "first line\nsecond line\n\n\n", + }, + want: "second line", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas), "RenderCommitBody(%v, %v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas) + }) + } +} diff --git a/modules/util/path.go b/modules/util/path.go index 1a68bc748802..58258560dded 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -222,6 +222,8 @@ func isOSWindows() bool { return runtime.GOOS == "windows" } +var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/") + // FileURLToPath extracts the path information from a file://... url. func FileURLToPath(u *url.URL) (string, error) { if u.Scheme != "file" { @@ -235,8 +237,7 @@ func FileURLToPath(u *url.URL) (string, error) { } // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash. - re := regexp.MustCompile("/[A-Za-z]:/") - if re.MatchString(path) { + if driveLetterRegexp.MatchString(path) { return path[1:], nil } return path, nil diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index 017ed45f8c04..ad0fb1a68b4a 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -15,7 +15,9 @@ import ( // 1563418 -> 2 weeks 4 days // 3937125s -> 1 month 2 weeks // 45677465s -> 1 year 6 months -func SecToTime(duration int64) string { +func SecToTime(durationVal any) string { + duration, _ := ToInt64(durationVal) + formattedTime := "" // The following four variables are calculated by taking diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 234b898fc1d2..ff59fbc96f9d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -129,6 +129,7 @@ concept_user_organization = Organization show_timestamps = Show timestamps show_log_seconds = Show seconds show_full_screen = Show full screen +download_logs = Download logs confirm_delete_selected = Confirm to delete all selected items? @@ -197,11 +198,9 @@ host = Host user = Username password = Password db_name = Database Name -db_helper = Note to MySQL users: please use the InnoDB storage engine and if you use "utf8mb4", your InnoDB version must be greater than 5.6 . db_schema = Schema db_schema_helper = Leave blank for database default ("public"). ssl_mode = SSL -charset = Charset path = Path sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if you run Gitea as a service. reinstall_error = You are trying to install into an existing Gitea database @@ -664,7 +663,7 @@ comment_type_group_project = Project comment_type_group_issue_ref = Issue reference saved_successfully = Your settings were saved successfully. privacy = Privacy -keep_activity_private = Hide the activity from the profile page +keep_activity_private = Hide Activity from profile page keep_activity_private_popup = Makes the activity visible only for you and the admins lookup_avatar_by_mail = Look Up Avatar by Email Address @@ -698,6 +697,7 @@ requires_activation = Requires activation primary_email = Make Primary activate_email = Send Activation activations_pending = Activations Pending +can_not_add_email_activations_pending = There is a pending activation, try again in a few minutes if you want to add a new email. delete_email = Remove email_deletion = Remove Email Address email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue? @@ -1613,6 +1613,9 @@ issues.review.pending.tooltip = This comment is not currently visible to other u issues.review.review = Review issues.review.reviewers = Reviewers issues.review.outdated = Outdated +issues.review.outdated_description = Content has changed since this comment was made +issues.review.option.show_outdated_comments = Show outdated comments +issues.review.option.hide_outdated_comments = Hide outdated comments issues.review.show_outdated = Show outdated issues.review.hide_outdated = Hide outdated issues.review.show_resolved = Show resolved @@ -2657,6 +2660,7 @@ dashboard.delete_repo_archives.started = Delete all repository archives task sta dashboard.delete_missing_repos = Delete all repositories missing their Git files dashboard.delete_missing_repos.started = Delete all repositories missing their Git files task started. dashboard.delete_generated_repository_avatars = Delete generated repository avatars +dashboard.sync_repo_branches = Sync missed branches from git data to databases dashboard.update_mirrors = Update Mirrors dashboard.repo_health_check = Health check all repositories dashboard.check_repo_stats = Check all repository statistics @@ -2710,6 +2714,7 @@ dashboard.gc_lfs = Garbage collect LFS meta objects dashboard.stop_zombie_tasks = Stop zombie tasks dashboard.stop_endless_tasks = Stop endless tasks dashboard.cancel_abandoned_jobs = Cancel abandoned jobs +dashboard.sync_branch.started = Branches Sync started users.user_manage_panel = User Account Management users.new_account = Create User Account @@ -2795,6 +2800,7 @@ repos.stars = Stars repos.forks = Forks repos.issues = Issues repos.size = Size +repos.lfs_size = LFS Size packages.package_manage_panel = Package Management packages.total_size = Total Size: %s diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 1818343f92fa..75971caa903c 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -3122,7 +3122,7 @@ notices.delete_success=ใ‚ทใ‚นใƒ†ใƒ ้€š็Ÿฅใ‚’ๅ‰Š้™คใ—ใพใ—ใŸใ€‚ [action] create_repo=ใŒใƒชใƒใ‚ธใƒˆใƒช %s ใ‚’ไฝœๆˆใ—ใพใ—ใŸ -rename_repo=ใŒใƒชใƒใ‚ธใƒˆใƒชๅใ‚’ %[1]s ใ‹ใ‚‰ [3]s ใธๅค‰ๆ›ดใ—ใพใ—ใŸ +rename_repo=ใŒใƒชใƒใ‚ธใƒˆใƒชๅใ‚’ %[1]s ใ‹ใ‚‰ %[3]s ใธๅค‰ๆ›ดใ—ใพใ—ใŸ commit_repo=ใŒ %[4]s ใฎ %[3]s ใซใƒ—ใƒƒใ‚ทใƒฅใ—ใพใ—ใŸ create_issue=`ใŒใ‚คใ‚ทใƒฅใƒผ %[3]s#%[2]s ใ‚’ใ‚ชใƒผใƒ—ใƒณใ—ใพใ—ใŸ` close_issue=`ใŒใ‚คใ‚ทใƒฅใƒผ %[3]s#%[2]s ใ‚’ใ‚ฏใƒญใƒผใ‚บใ—ใพใ—ใŸ` diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index ccfbcbf98a2b..dfffb92d2f67 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -79,6 +79,8 @@ milestones=Marcos ok=Ok cancel=Cancelar +rerun=Reexecutar +rerun_all=Reexecutar todas as tarefas save=Salvar add=Adicionar add_all=Adicionar todos @@ -113,11 +115,18 @@ unknown=Desconhecido rss_feed=Feed RSS +pin=Fixar +unpin=Desfixar +artifacts=Artefatos +concept_system_global=Global +concept_user_individual=Individual concept_code_repository=Repositรณrio concept_user_organization=Organizaรงรฃo +show_log_seconds=Mostrar segundos +show_full_screen=Mostrar tela cheia [aria] navbar=Barra de navegaรงรฃo @@ -142,6 +151,7 @@ buttons.list.unordered.tooltip=Adicionar uma lista com marcadores buttons.list.ordered.tooltip=Adicionar uma lista numerada buttons.list.task.tooltip=Adicionar uma lista de tarefas buttons.mention.tooltip=Mencionar um usuรกrio ou equipe +buttons.ref.tooltip=Referenciar um issue ou um pull request buttons.switch_to_legacy.tooltip=Em vez disso, usar o editor legado buttons.enable_monospace_font=Habilitar fonte mono espaรงada buttons.disable_monospace_font=Desabilitar fonte mono espaรงada @@ -247,6 +257,7 @@ openid_signup_popup=Habilitar o auto-cadastro com base no OpenID. enable_captcha=Habilitar CAPTCHA ao registrar enable_captcha_popup=Obrigar validaรงรฃo por CAPTCHA para auto-cadastro de usuรกrios. require_sign_in_view=Exigir acesso do usuรกrio para a visualizaรงรฃo de pรกginas +require_sign_in_view_popup=Limitar o acesso de pรกgina aos usuรกrios autenticados. Os visitantes sรณ verรฃo as pรกginas de autenticaรงรฃo e cadastro. admin_setting_desc=Criar uma conta de administrador รฉ opcional. O primeiro usuรกrio cadastrado automaticamente se tornarรก um administrador. admin_title=Configuraรงรตes da conta de administrador admin_name=Nome do usuรกrio administrador @@ -312,6 +323,7 @@ repos=Repositรณrios users=Usuรกrios organizations=Organizaรงรตes search=Pesquisar +go_to=Ir para code=Cรณdigo search.type.tooltip=Tipo de pesquisa search.fuzzy=Similar @@ -467,6 +479,7 @@ team_invite.text_3=Nota: este convite foi destinado a %[1]s. Se vocรช nรฃo estav [modal] yes=Sim no=Nรฃo +confirm=Confirmar cancel=Cancelar modify=Atualizar @@ -514,6 +527,7 @@ lang_select_error=Selecione um idioma da lista. username_been_taken=O nome de usuรกrio jรก estรก sendo usado. username_change_not_local_user=Usuรกrios nรฃo-locais nรฃo sรฃo autorizados a alterar nome de usuรกrio. +username_has_not_been_changed=Nome de usuรกrio nรฃo foi alterado repo_name_been_taken=O nome de repositรณrio jรก estรก sendo usado. repository_force_private=Forรงar Privado estรก ativado: repositรณrios privados nรฃo podem ser tornados pรบblicos. repository_files_already_exist=Arquivos jรก existem neste repositรณrio. Contate o administrador. @@ -555,11 +569,14 @@ auth_failed=Autenticaรงรฃo falhou: %v still_own_repo=Sua conta possui um ou mais repositรณrios, exclua ou transfira-os primeiro. still_has_org=Sua conta รฉ um membro de uma ou mais organizaรงรตes, deixe-as primeiro. still_own_packages=Sua conta possui um ou mais pacotes, exclua-os primeiro. +org_still_own_repo=Esta organizaรงรฃo ainda possui repositรณrios, exclua ou transfira-os primeiro. +org_still_own_packages=Esta organizaรงรฃo ainda possui pacotes, exclua-os primeiro. target_branch_not_exist=O branch de destino nรฃo existe. [user] change_avatar=Altere seu avatar... +joined_on=Inscreveu-se em %s repositories=Repositรณrios activity=Atividade pรบblica followers=Seguidores @@ -684,10 +701,12 @@ add_new_email=Adicionar novo endereรงo de e-mail add_new_openid=Adicionar novo URI OpenID add_email=Adicionar novo endereรงo de e-mail add_openid=Adicionar URI OpenID +add_email_confirmation_sent=Um e-mail de confirmaรงรฃo foi enviado para "%s". Verifique sua caixa de entrada nos prรณximos %s para confirmar seu endereรงo de e-mail. add_email_success=O novo endereรงo de e-mail foi adicionado. email_preference_set_success=Preferรชncia de e-mail definida com sucesso. add_openid_success=O novo endereรงo de OpenID foi adicionado. keep_email_private=Ocultar endereรงo de e-mail +keep_email_private_popup=Seu endereรงo de e-mail ficarรก visรญvel apenas para vocรช e para os administradores openid_desc=OpenID permite delegar autenticaรงรฃo para um provedor externo. manage_ssh_keys=Gerenciar Chaves SSH @@ -721,6 +740,7 @@ gpg_token_help=Vocรช pode gerar uma assinatura usando: gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Assinatura GPG blindada key_signature_gpg_placeholder=Comeรงa com '-----BEGIN PGP SIGNATURE-----' +verify_gpg_key_success=A chave GPG "%s" foi validada. ssh_key_verified=Chave validada ssh_key_verified_long=A chave foi validada com um token e pode ser usada para validar commits que correspondam a qualquer dos endereรงos de e-mail ativados deste usuรกrio. ssh_key_verify=Validar @@ -730,11 +750,14 @@ ssh_token=Token ssh_token_help=Vocรช pode gerar uma assinatura usando: ssh_token_signature=Assinatura SSH blindada key_signature_ssh_placeholder=Comeรงa com '-----BEGIN SSH SIGNATURE-----' +verify_ssh_key_success=A chave SSH "%s" foi validada. subkeys=Subchaves key_id=ID da chave key_name=Nome da Chave key_content=Conteรบdo principal_content=Conteรบdo +add_key_success=A chave SSH "%s" foi adicionada. +add_gpg_key_success=A chave GPG "%s" foi adicionada. delete_key=Remover ssh_key_deletion=Remover a chave SSH gpg_key_deletion=Remover a chave GPG @@ -745,6 +768,8 @@ ssh_principal_deletion_desc=A exclusรฃo de um Nome Principal de um Certificado S ssh_key_deletion_success=A chave SSH foi removida. gpg_key_deletion_success=A chave GPG foi removida. ssh_principal_deletion_success=O nome principal foi removido. +added_on=Adicionado em %s +valid_until_date=Vรกlido atรฉ %s valid_forever=Vรกlido para sempre last_used=รšltima vez usado em no_activity=Nenhuma atividade recente @@ -756,6 +781,7 @@ principal_state_desc=Este nome principal foi utilizado nos รบltimos 7 dias show_openid=Mostrar no perfil hide_openid=Ocultar no perfil ssh_disabled=SSH desabilitado +ssh_signonly=O SSH estรก desativado no momento, portanto, essas chaves sรฃo usadas apenas para verificaรงรฃo de assinatura de confirmaรงรฃo. ssh_externally_managed=Esta chave SSH para este usuรกrio รฉ gerenciada externamente manage_social=Gerenciar contas sociais associadas social_desc=Essas contas sociais estรฃo vinculadas ร  sua conta do Gitea. Certifique-se de reconhecer todas elas, pois elas podem ser usadas para acessar a sua conta do Gitea. @@ -775,6 +801,12 @@ access_token_deletion_cancel_action=Cancelar access_token_deletion_confirm_action=Excluir access_token_deletion_desc=A exclusรฃo de um token revoga o acesso ร  sua conta para aplicativos que o usam. Continuar? delete_token_success=O token foi excluรญdo. Os aplicativos que o utilizam jรก nรฃo tรชm acesso ร  sua conta. +repo_and_org_access=Acesso ao Repositรณrio e Organizaรงรฃo +permissions_public_only=Apenas pรบblico +permissions_access_all=Todos (pรบblico, privado e limitado) +select_permissions=Selecionar permissรตes +at_least_one_permission=Vocรช deve selecionar pelo menos uma permissรฃo para criar um token +permissions_list=Permissรตes: manage_oauth2_applications=Gerenciar aplicativos OAuth2 edit_oauth2_application=Editar aplicativo OAuth2 @@ -859,6 +891,7 @@ visibility=Visibilidade do usuรกrio visibility.public=Pรบblica visibility.public_tooltip=Visรญvel para todos visibility.limited=Limitada +visibility.limited_tooltip=Visรญvel apenas para usuรกrios autenticados visibility.private=Privada visibility.private_tooltip=Visรญvel apenas para membros da organizaรงรฃo @@ -932,6 +965,7 @@ mirror_password_blank_placeholder=(nรฃo definida) mirror_password_help=Altere o nome de usuรกrio para apagar uma senha armazenada. watchers=Observadores stargazers=Usuรกrios que estrelaram +stars_remove_warning=Isto irรก remover todos os favoritos dados a este repositรณrio. forks=Forks reactions_more=e %d mais unit_disabled=O administrador do site desabilitou esta seรงรฃo do repositรณrio. @@ -946,6 +980,7 @@ delete_preexisting=Excluir arquivos prรฉ-existentes delete_preexisting_content=Excluir arquivos em %s delete_preexisting_success=Arquivos รณrfรฃos excluรญdos em %s blame_prior=Ver a responsabilizaรงรฃo anterior a esta modificaรงรฃo +author_search_tooltip=Mostra um mรกximo de 30 usuรกrios transfer.accept=Aceitar transferรชncia transfer.accept_desc=`Transferir para "%s"` @@ -974,11 +1009,14 @@ template.one_item=Deve-se selecionar pelo menos um item de modelo template.invalid=Deve-se selecionar um repositรณrio de modelo archive.title=Este repositรณrio estรก arquivado. Vocรช pode visualizar os arquivos e realizar clone, mas nรฃo poderรก realizar push nem abrir issues e pull requests. +archive.title_date=Este repositรณrio foi arquivado em %s. Vocรช pode visualizar os arquivos e realizar clone, mas nรฃo poderรก realizar push nem abrir issues e pull requests. archive.issue.nocomment=Este repositรณrio estรก arquivado. Vocรช nรฃo pode comentar nas issues. archive.pull.nocomment=Este repositรณrio estรก arquivado. Vocรช nรฃo pode comentar nos pull requests. form.reach_limit_of_creation_1=Vocรช jรก atingiu o seu limite de %d repositรณrio. form.reach_limit_of_creation_n=Vocรช jรก atingiu o limite de %d repositรณrios. +form.name_reserved=O nome de repositรณrio "%s" estรก reservado. +form.name_pattern_not_allowed=O padrรฃo "%s" nรฃo รฉ permitido em um nome de repositรณrio. need_auth=Autorizaรงรฃo migrate_options=Opรงรตes de Migraรงรฃo @@ -1004,6 +1042,7 @@ migrate.github_token_desc=Vocรช pode colocar aqui um ou mais tokens separados po migrate.clone_local_path=ou um caminho de servidor local migrate.permission_denied=Vocรช nรฃo pode importar repositรณrios locais. migrate.permission_denied_blocked=Vocรช nรฃo pode importar dos hosts nรฃo permitidos, por favor peรงa ao administrador para verificar as configuraรงรตes ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. +migrate.invalid_local_path=O caminho local รฉ invรกlido. Ele nรฃo existe ou nรฃo รฉ um diretรณrio. migrate.invalid_lfs_endpoint=O destino LFS nรฃo รฉ vรกlido. migrate.failed=Migraรงรฃo falhou: %v migrate.migrate_items_options=Um Token de Acesso รฉ necessรกrio para migrar itens adicionais @@ -1012,6 +1051,7 @@ migrated_from_fake=Migrado de %[1]s migrate.migrate=Migrar de %s migrate.migrating=Migrando a partir de %s ... migrate.migrating_failed=Migraรงรฃo a partir de %s falhou. +migrate.migrating_failed.error=Falha ao migrar: %s migrate.migrating_failed_no_addr=A migraรงรฃo falhou. migrate.github.description=Migrar dados de github.com ou de outras instรขncias do GitHub. migrate.git.description=Migrar um repositรณrio somente de qualquer serviรงo Git. @@ -1028,6 +1068,8 @@ migrate.migrating_labels=Migrando Rรณtulos migrate.migrating_releases=Migrando Versรตes migrate.migrating_issues=Migrando Issues migrate.migrating_pulls=Migrando Pull Requests +migrate.cancel_migrating_title=Cancelar migraรงรฃo +migrate.cancel_migrating_confirm=Vocรช quer cancelar essa migraรงรฃo? mirror_from=espelhamento de forked_from=feito fork de @@ -1112,6 +1154,7 @@ download_file=Baixar arquivo normal_view=Visรฃo normal line=linha lines=linhas +from_comment=(comentรกrio) editor.add_file=Adicionar Arquivo editor.new_file=Novo arquivo @@ -1126,6 +1169,7 @@ editor.must_be_on_a_branch=Vocรช deve estar em um branch para propor alteraรงรตe editor.fork_before_edit=Vocรช deve fazer um fork desse repositรณrio para fazer ou propor alteraรงรตes neste arquivo. editor.delete_this_file=Excluir arquivo editor.must_have_write_access=Vocรช deve ter permissรฃo de escrita para fazer ou propor alteraรงรตes neste arquivo. +editor.file_delete_success=O arquivo "%s" foi excluรญdo. editor.name_your_file=Nomeie o seu arquivoโ€ฆ editor.filename_help=Adicione um diretรณrio digitando seu nome seguido por uma barra ('/'). Remova um diretรณrio digitando o backspace no inรญcio do campo de entrada. editor.or=ou @@ -1133,8 +1177,12 @@ editor.cancel_lower=Cancelar editor.commit_signed_changes=Commit de alteradores assinadas editor.commit_changes=Aplicar commit das alteraรงรตes editor.add_tmpl=Adicionar '' +editor.add=Adicionar %s +editor.update=Atualizar %s +editor.delete=Excluir %s editor.patch=Aplicar Correรงรฃo editor.patching=Corrigindo: +editor.fail_to_apply_patch=`Nรฃo foi possรญvel aplicar a correรงรฃo "%s"` editor.new_patch=Nova correรงรฃo editor.commit_message_desc=Adicione uma descriรงรฃo detalhada (opcional)... editor.signoff_desc=Adicione um assinado-por-committer no final do log do commit. @@ -1146,15 +1194,29 @@ editor.new_branch_name=Nome do novo branch para este commit editor.new_branch_name_desc=Novo nome do branch... editor.cancel=Cancelar editor.filename_cannot_be_empty=Nome do arquivo nรฃo pode ser em branco. +editor.filename_is_invalid=O nome do arquivo รฉ invรกlido: "%s". +editor.branch_does_not_exist=Branch "%s" nรฃo existe neste repositรณrio. +editor.branch_already_exists=Branch "%s" jรก existe neste repositรณrio. +editor.directory_is_a_file=O nome do diretรณrio "%s" jรก รฉ usado como um nome de arquivo neste repositรณrio. +editor.file_is_a_symlink=`"%s" รฉ um link simbรณlico. Links simbรณlicos nรฃo podem ser editados no editor da web` +editor.filename_is_a_directory=O nome do arquivo "%s" jรก รฉ usado como um nome de diretรณrio neste repositรณrio. +editor.file_editing_no_longer_exists=O arquivo que estรก sendo editado, "%s", nรฃo existe mais neste repositรณrio. +editor.file_deleting_no_longer_exists=O arquivo a ser excluรญdo, "%s", nรฃo existe mais neste repositรณrio. editor.file_changed_while_editing=O conteรบdo do arquivo mudou desde que vocรช comeรงou a editar. Clique aqui para ver o que foi editado ou clique em Aplicar commit das alteraรงรตes novamemente para sobreescrever estas alteraรงรตes. +editor.file_already_exists=Um arquivo com nome "%s" jรก existe neste repositรณrio. editor.commit_empty_file_header=Fazer commit de um arquivo vazio editor.commit_empty_file_text=O arquivo que vocรช estรก prestes fazer commit estรก vazio. Continuar? editor.no_changes_to_show=Nenhuma alteraรงรฃo a mostrar. +editor.fail_to_update_file=Falha ao atualizar/criar arquivo "%s". editor.fail_to_update_file_summary=Mensagem de erro: editor.push_rejected_no_message=A alteraรงรฃo foi rejeitada pelo servidor sem uma mensagem. Por favor, verifique os Hooks Git. editor.push_rejected=A alteraรงรฃo foi rejeitada pelo servidor. Por favor, verifique os Hooks Git. editor.push_rejected_summary=Mensagem completa de rejeiรงรฃo: editor.add_subdir=Adicionar um subdiretรณrio... +editor.unable_to_upload_files=Ocorreu um erro ao enviar arquivos para "%s": %v +editor.upload_file_is_locked=Arquivo "%s" estรก bloqueado por %s. +editor.upload_files_to_dir=`Enviar arquivos para "%s"` +editor.cannot_commit_to_protected_branch=Nรฃo foi possรญvel enviar commits para o branch protegido "%s". editor.no_commit_to_branch=Nรฃo foi possรญvel fazer commit diretamente no branch porque: editor.user_no_push_to_branch=O usuรกrio nรฃo pode fazer push no branch editor.require_signed_commit=Branch requer um commit assinado @@ -1163,6 +1225,7 @@ editor.revert=Reverter %s para: commits.desc=Veja o histรณrico de alteraรงรตes do cรณdigo de fonte. commits.commits=Commits +commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem histรณricos completamente diferentes. commits.nothing_to_compare=Estes branches sรฃo iguais. commits.search=Pesquisar commits... commits.find=Pesquisar @@ -1197,12 +1260,14 @@ projects.create=Criar Projeto projects.title=Tรญtulo projects.new=Novo projeto projects.new_subheader=Coordene, acompanhe e atualize seu trabalho em um sรณ lugar, para que os projetos permaneรงam transparentes e dentro do cronograma. +projects.create_success=Projeto "%s" criado. projects.deletion=Apagar Projeto projects.deletion_desc=Excluir um projeto o remove de todas as issues relacionadas. Deseja continuar? projects.deletion_success=O projeto foi excluido. projects.edit=Editar Projetos projects.edit_subheader=Projetos organizam issues e acompanham o progresso. projects.modify=Atualizar Projeto +projects.edit_success=Projeto "%s" atualizado. projects.type.none=Nenhum projects.type.basic_kanban=Kanban bรกsico projects.type.bug_triage=Triagem de Bugs @@ -1295,6 +1360,10 @@ issues.filter_label_exclude=`Use alt + clique/enter pa issues.filter_label_no_select=Todas as etiquetas issues.filter_label_select_no_label=Sem etiqueta issues.filter_milestone=Marco +issues.filter_milestone_all=Todos os marcos +issues.filter_milestone_none=Sem marcos +issues.filter_milestone_open=Marcos abertos +issues.filter_milestone_closed=Marcos fechados issues.filter_project=Projeto issues.filter_project_all=Todos os projetos issues.filter_project_none=Sem projeto @@ -1353,6 +1422,7 @@ issues.context.reference_issue=Referรชncia em uma nova issue issues.context.edit=Editar issues.context.delete=Excluir issues.no_content=Ainda nรฃo hรก conteรบdo. +issues.close=Fechar issue issues.close_comment_issue=Comentar e fechar issues.reopen_issue=Reabrir issues.reopen_comment_issue=Comentar e reabrir @@ -1403,6 +1473,10 @@ issues.attachment.open_tab=`Clique para ver "%s" em uma nova aba` issues.attachment.download=`Clique para baixar "%s"` issues.subscribe=Inscrever-se issues.unsubscribe=Desinscrever +issues.unpin_issue=Desfixar issue +issues.max_pinned=Vocรช nรฃo pode fixar mais issues +issues.pin_comment=fixou isto %s +issues.unpin_comment=desafixou isto %s issues.lock=Bloquear conversaรงรฃo issues.unlock=Desbloquear conversaรงรฃo issues.lock.unknown_reason=Nรฃo pode-se bloquear uma issue com um motivo desconhecido. @@ -1468,6 +1542,9 @@ issues.due_date_invalid=A data limite รฉ invรกlida ou estรก fora do intervalo. P issues.dependency.title=Dependรชncias issues.dependency.issue_no_dependencies=Nenhuma dependรชncia definida. issues.dependency.pr_no_dependencies=Nenhuma dependรชncia definida. +issues.dependency.no_permission_1=Vocรช nรฃo tem permissรฃo para ler %d dependรชncia +issues.dependency.no_permission_n=Vocรช nรฃo tem permissรฃo para ler %d dependรชncias +issues.dependency.no_permission.can_remove=Vocรช nรฃo tem permissรฃo para ler esta dependรชncia, mas pode remover esta dependรชncia issues.dependency.add=Adicioneโ€ฆ issues.dependency.cancel=Cancelar issues.dependency.remove=Remover @@ -1506,6 +1583,7 @@ issues.review.add_review_request=solicitou revisรฃo de %s %s issues.review.remove_review_request=removeu a solicitaรงรฃo de revisรฃo para %s %s issues.review.remove_review_request_self=recusou revisar %s issues.review.pending=Pendente +issues.review.pending.tooltip=Este comentรกrio nรฃo estรก atualmente visรญvel para outros usuรกrios. Para enviar seus comentรกrios pendentes, selecione "%s" -> "%s/%s/%s" no topo da pรกgina. issues.review.review=Revisรฃo issues.review.reviewers=Revisores issues.review.outdated=Desatualizado @@ -1540,6 +1618,8 @@ pulls.compare_changes_desc=Selecione o branch de destino (push) e o branch de or pulls.has_viewed_file=Visto pulls.has_changed_since_last_review=Alterado desde a รบltima revisรฃo pulls.viewed_files_label=%[1]d / %[2]d arquivos visualizados +pulls.expand_files=Expandir todos os arquivos +pulls.collapse_files=Colapsar todos os arquivos pulls.compare_base=merge em pulls.compare_compare=pull de pulls.switch_comparison_type=Mudar tipo de comparaรงรฃo @@ -1560,6 +1640,7 @@ pulls.reopen_to_merge=Por favor reabra este pull request para aplicar o merge. pulls.cant_reopen_deleted_branch=Este pull request nรฃo pode ser reaberto porque o branch foi excluรญdo. pulls.merged=Merge aplicado pulls.manually_merged=Merge aplicado manualmente +pulls.merged_info_text=O branch %s pode ser excluรญdo. pulls.is_closed=O pull request foi fechado. pulls.title_wip_desc=`Inicie o tรญtulo com o prefixo %s para prevenir o merge do pull request atรฉ que o mesmo esteja pronto.` pulls.cannot_merge_work_in_progress=Este pull request estรก marcado como um trabalho em andamento. @@ -1630,6 +1711,7 @@ pulls.update_branch_rebase=Atualizar branch por rebase pulls.update_branch_success=Atualizaรงรฃo do branch foi bem-sucedida pulls.update_not_allowed=Vocรช nรฃo tem permissรฃo para atualizar o branch pulls.outdated_with_base_branch=Este branch estรก desatualizado com o branch base +pulls.close=Fechar pull request pulls.closed_at=`fechou este pull request %[2]s` pulls.reopened_at=`reabriu este pull request %[2]s` pulls.merge_instruction_hint=`Vocรช tambรฉm pode ver as instruรงรตes para a linha de comandos.` @@ -1655,6 +1737,7 @@ pulls.delete.text=Vocรช realmente deseja excluir este pull request? (Isto irรก r milestones.new=Novo marco milestones.closed=Fechado %s +milestones.update_ago=Atualizado hรก %s milestones.no_due_date=Sem data limite milestones.open=Reabrir milestones.close=Fechar @@ -1666,10 +1749,12 @@ milestones.desc=Descriรงรฃo milestones.due_date=Data limite (opcional) milestones.clear=Limpar milestones.invalid_due_date_format=Formato da data limite deve ser 'dd/mm/aaaa'. +milestones.create_success=O marco "%s" foi criado. milestones.edit=Editar marco milestones.edit_subheader=Marcos organizam as issues e acompanham o progresso. milestones.cancel=Cancelar milestones.modify=Atualizar marco +milestones.edit_success=O marco "%s" foi atualizado. milestones.deletion=Excluir marco milestones.deletion_desc=A exclusรฃo deste marco irรก removรช-lo de todas as issues. Tem certeza que deseja continuar? milestones.deletion_success=O marco foi excluรญdo. @@ -1680,6 +1765,7 @@ milestones.filter_sort.most_complete=Mais completo milestones.filter_sort.most_issues=Com mais issues milestones.filter_sort.least_issues=Com menos issues +signing.will_sign=`Este commit serรก assinado com a chave "%s"` signing.wont_sign.error=Houve um erro ao verificar se o commit poderia ser assinado signing.wont_sign.nokey=Nรฃo hรก chave disponรญvel para assinar este commit signing.wont_sign.never=Commits nunca sรฃo assinados @@ -1704,6 +1790,8 @@ wiki.create_first_page=Criar a primeira pรกgina wiki.page=Pรกgina wiki.filter_page=Filtrar pรกgina wiki.new_page=Pรกgina +wiki.page_title=Tรญtulo da pรกgina +wiki.page_content=Conteรบdo wiki.default_commit_message=Escreva uma nota sobre a atualizaรงรฃo nesta pรกgina (opcional). wiki.save_page=Salvar pรกgina wiki.last_commit_info=%s editou esta pรกgina %s @@ -1808,6 +1896,7 @@ settings.hooks=Webhooks settings.githooks=Hooks do Git settings.basic_settings=Configuraรงรตes bรกsicas settings.mirror_settings=Opรงรตes de espelhamento +settings.mirror_settings.docs.doc_link_title=Como posso espelhar repositรณrios? settings.mirror_settings.mirrored_repository=Repositรณrio espelhado settings.mirror_settings.direction=Sentido settings.mirror_settings.direction.pull=Pull @@ -2100,8 +2189,11 @@ settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteรบdo settings.require_signed_commits=Exibir commits assinados settings.require_signed_commits_desc=Rejeitar pushes para este branch se nรฃo estiverem assinados ou nรฃo forem validรกveis. settings.protect_branch_name_pattern=Padrรฃo de Nome de Branch Protegida +settings.protect_protected_file_patterns=Padrรตes de arquivos protegidos (separados usando ponto e vรญrgula ';'): settings.add_protected_branch=Habilitar proteรงรฃo settings.delete_protected_branch=Desabilitar proteรงรฃo +settings.remove_protected_branch_success=Proteรงรฃo do branch "%s" foi desabilitada. +settings.remove_protected_branch_failed=Removendo regra de proteรงรฃo de branch "%s" falhou. settings.protected_branch_deletion=Desabilitar proteรงรฃo de branch settings.protected_branch_deletion_desc=Desabilitar a proteรงรฃo de branch permite que os usuรกrios com permissรฃo de escrita realizem push. Continuar? settings.block_rejected_reviews=Bloquear merge em revisรตes rejeitadas @@ -2224,7 +2316,9 @@ diff.review.header=Enviar revisรฃo diff.review.placeholder=Comentรกrio da revisรฃo diff.review.comment=Comentar diff.review.approve=Aprovar +diff.review.self_reject=Os autores do pull request nรฃo podem solicitar alteraรงรตes em seus prรณprios pull request diff.review.reject=Solicitar alteraรงรตes +diff.review.self_approve=Os autores do pull request nรฃo podem aprovar seu prรณprio pull request diff.committed_by=commit de diff.protected=Protegido diff.image.side_by_side=Lado a Lado @@ -2290,6 +2384,7 @@ branch.delete=`Excluir branch "%s"` branch.delete_html=Excluir Branch branch.delete_desc=A exclusรฃo de um branch รฉ permanente. Isto NรƒO PODERร ser desfeito. Continuar? branch.create_branch=Criar branch %s +branch.branch_already_exists=Branch "%s" jรก existe neste repositรณrio. branch.deleted_by=Excluรญdo por %s branch.included_desc=Este branch faz parte do branch padrรฃo branch.included=Incluรญdo @@ -2885,6 +2980,7 @@ config.xorm_log_sql=Log SQL config.get_setting_failed=Falha ao obter configuraรงรฃo %s config.set_setting_failed=Falha ao definir configuraรงรฃo %s +monitor.stats=Estatรญsticas monitor.cron=Tarefas cron monitor.name=Nome @@ -2894,6 +2990,8 @@ monitor.previous=Vez anterior monitor.execute_times=Execuรงรตes monitor.process=Processos em execuรงรฃo monitor.stacktrace=Stacktraces +monitor.processes_count=%d processos +monitor.download_diagnosis_report=Baixar relatรณrio de diagnรณstico monitor.desc=Descriรงรฃo monitor.start=Hora de inรญcio monitor.execute_time=Tempo de execuรงรฃo @@ -2919,6 +3017,7 @@ monitor.queue.settings.maxnumberworkers.placeholder=Atualmente %[1]d monitor.queue.settings.maxnumberworkers.error=Nรบmero mรกximo de executores deve ser um nรบmero monitor.queue.settings.submit=Atualizar configuraรงรตes monitor.queue.settings.changed=Configuraรงรตes atualizadas +monitor.queue.settings.remove_all_items=Remover tudo notices.system_notice_list=Avisos do sistema notices.view_detail_header=Ver detalhes do aviso @@ -3053,8 +3152,10 @@ versions.view_all=Ver todas dependency.id=ID dependency.version=Versรฃo alpine.install=Para instalar o pacote, execute o seguinte comando: +alpine.repository=Informaรงรตes do repositรณrio alpine.repository.branches=Branches alpine.repository.repositories=Repositรณrios +alpine.repository.architectures=Arquiteturas cargo.registry=Configurar este registro no arquivo de configuraรงรฃo de Cargo (por exemplo ~/.cargo/config.toml): cargo.install=Para instalar o pacote usando Cargo, execute o seguinte comando: cargo.documentation=Para obter mais informaรงรตes sobre o registro Cargo, consulte a documentaรงรฃo. @@ -3089,7 +3190,12 @@ container.labels.key=Chave container.labels.value=Valor cran.install=Para instalar o pacote, execute o seguinte comando: debian.registry=Configure este registro pela linha de comando: +debian.registry.info=Escolha uma $distribution e um $component da lista abaixo: debian.install=Para instalar o pacote, execute o seguinte comando: +debian.repository=Informaรงรตes do repositรณrio +debian.repository.distributions=Distribuiรงรตes +debian.repository.components=Componentes +debian.repository.architectures=Arquiteturas generic.download=Baixar pacote pela linha de comando: generic.documentation=Para obter mais informaรงรตes sobre o registro genรฉrico, consulte a documentaรงรฃo. helm.registry=Configurar este registro pela linha de comando: @@ -3242,13 +3348,18 @@ runners.status.idle=Inativo runners.status.active=Ativo runners.status.offline=Offiline runners.version=Versรฃo +runners.reset_registration_token_success=Token de registro de runner redefinido com sucesso runs.all_workflows=Todos os Workflows runs.commit=Commit runs.pushed_by=Push realizado por runs.invalid_workflow_helper=O arquivo de configuraรงรฃo do workflow รฉ invรกlido. Por favor, verifique seu arquivo de configuraรงรฃo: %s +runs.no_matching_runner_helper=Nenhum runner correspondente: %s need_approval_desc=Precisa de aprovaรงรฃo para executar workflows para pull request do fork. [projects] +type-1.display_name=Projeto individual +type-2.display_name=Projeto do repositรณrio +type-3.display_name=Projeto da organizaรงรฃo diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 03e191ba4664..3688a42d71fc 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -120,11 +120,14 @@ unpin=ะžั‚ะบั€ะตะฟะธั‚ัŒ artifacts=ะั€ั‚ะตั„ะฐะบั‚ั‹ +concept_system_global=ะ“ะปะพะฑะฐะปัŒะฝะพ +concept_user_individual=ะ˜ะฝะดะธะฒะธะดัƒะฐะปัŒะฝะพ concept_code_repository=ะ ะตะฟะพะทะธั‚ะพั€ะธะน concept_user_organization=ะžั€ะณะฐะฝะธะทะฐั†ะธั show_timestamps=ะžั‚ะพะฑั€ะฐะถะฐั‚ัŒ ะฒั€ะตะผั show_log_seconds=ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ัะตะบัƒะฝะดั‹ +show_full_screen=ะŸะพะบะฐะทะฐั‚ัŒ ะฒะพ ะฒะตััŒ ัะบั€ะฐะฝ [aria] navbar=ะŸะฐะฝะตะปัŒ ะฝะฐะฒะธะณะฐั†ะธะธ @@ -133,6 +136,8 @@ footer.software=ะž ะฟั€ะพะณั€ะฐะผะผะต footer.links=ะกัั‹ะปะบะธ [heatmap] +number_of_contributions_in_the_last_12_months=ะŸั€ะธะฝะธะผะฐะป(ะฐ) ัƒั‡ะฐัั‚ะธะต %s ั€ะฐะท ะทะฐ ะฟะพัะปะตะดะฝะธะต 12 ะผะตััั†ะตะฒ +no_contributions=ะะต ะฟั€ะธะฝะธะผะฐะป(ะฐ) ัƒั‡ะฐัั‚ะธั less=ะœะตะฝัŒัˆะต more=ะ‘ะพะปัŒัˆะต @@ -572,6 +577,7 @@ target_branch_not_exist=ะฆะตะปะตะฒะฐั ะฒะตั‚ะบะฐ ะฝะต ััƒั‰ะตัั‚ะฒัƒะตั‚. [user] change_avatar=ะ˜ะทะผะตะฝะธั‚ัŒ ัะฒะพะน ะฐะฒะฐั‚ะฐั€โ€ฆ +joined_on=ะŸั€ะธัะพะตะดะธะฝะธะป(ัั/ะฐััŒ) %s repositories=ะ ะตะฟะพะทะธั‚ะพั€ะธะธ activity=ะะบั‚ะธะฒะฝะพัั‚ัŒ followers=ะŸะพะดะฟะธัั‡ะธะบะธ @@ -767,6 +773,8 @@ ssh_principal_deletion_desc=ะฃะดะฐะปะตะฝะธะต ะฟั€ะธะฝั†ะธะฟะฐะปะฐ ัะตั€ั‚ะธั„ะธ ssh_key_deletion_success=ะšะปัŽั‡ SSH ัƒะดะฐะปะตะฝ. gpg_key_deletion_success=ะšะปัŽั‡ GPG ัƒะดะฐะปั‘ะฝ. ssh_principal_deletion_success=ะŸั€ะธะฝั†ะธะฟะฐะป ัƒะดะฐะปั‘ะฝ. +added_on=ะ”ะพะฑะฐะฒะปะตะฝะพ %s +valid_until_date=ะ”ะตะนัั‚ะฒะธั‚ะตะปัŒะฝะพ ะดะพ %s valid_forever=ะ”ะตะนัั‚ะฒะธั‚ะตะปะตะฝ ะฝะฐะฒัะตะณะดะฐ last_used=ะŸะพัะปะตะดะฝะธะน ั€ะฐะท ะธัะฟะพะปัŒะทะพะฒะฐะปัั no_activity=ะ•ั‰ะต ะฝะต ะฟั€ะธะผะตะฝัะปัั @@ -798,7 +806,13 @@ access_token_deletion_cancel_action=ะžั‚ะผะตะฝะธั‚ัŒ access_token_deletion_confirm_action=ะฃะดะฐะปะธั‚ัŒ access_token_deletion_desc=ะฃะดะฐะปะตะฝะธะต ั‚ะพะบะตะฝะฐ ะพั‚ะทะพะฒั‘ั‚ ะดะพัั‚ัƒะฟ ะบ ะฒะฐัˆะตะน ัƒั‡ะตั‚ะฝะพะน ะทะฐะฟะธัะธ ัƒ ะฟั€ะธะปะพะถะตะฝะธะน, ะธัะฟะพะปัŒะทัƒัŽั‰ะธั… ะตะณะพ. ะญั‚ะพ ะดะตะนัั‚ะฒะธะต ะฝะต ะผะพะถะตั‚ ะฑั‹ั‚ัŒ ะพั‚ะผะตะฝะตะฝะพ. ะŸั€ะพะดะพะปะถะธั‚ัŒ? delete_token_success=ะขะพะบะตะฝ ัƒะดะฐะปั‘ะฝ. ะŸั€ะธะปะพะถะตะฝะธั, ะธัะฟะพะปัŒะทัƒัŽั‰ะธะต ะตะณะพ, ะฑะพะปัŒัˆะต ะฝะต ะธะผะตัŽั‚ ะดะพัั‚ัƒะฟะฐ ะบ ะฒะฐัˆะตะผัƒ ะฐะบะบะฐัƒะฝั‚ัƒ. +repo_and_org_access=ะ”ะพัั‚ัƒะฟ ะบ ั€ะตะฟะพะทะธั‚ะพั€ะธัŽ ะธ ะพั€ะณะฐะฝะธะทะฐั†ะธะธ +permissions_public_only=ะขะพะปัŒะบะพ ะฟัƒะฑะปะธั‡ะฝั‹ะต +permissions_access_all=ะ’ัะต (ะฟัƒะฑะปะธั‡ะฝั‹ะต, ะฟั€ะธะฒะฐั‚ะฝั‹ะต ะธ ะพะณั€ะฐะฝะธั‡ะตะฝะฝั‹ะต) +select_permissions=ะ’ั‹ะฑั€ะฐั‚ัŒ ั€ะฐะทั€ะตัˆะตะฝะธั scoped_token_desc=ะ’ั‹ะฑั€ะฐะฝะฝั‹ะต ะฟะพะปะฝะพะผะพั‡ะธั ั‚ะพะบะตะฝะฐ ะพะณั€ะฐะฝะธั‡ะธะฒะฐัŽั‚ ะฐัƒั‚ะตะฝั‚ะธั„ะธะบะฐั†ะธัŽ ั‚ะพะปัŒะบะพ ัะพะพั‚ะฒะตั‚ัั‚ะฒัƒัŽั‰ะธะผะธ ะผะฐั€ัˆั€ัƒั‚ะฐะผะธ API. ะงะธั‚ะฐะนั‚ะต ะดะพะบัƒะผะตะฝั‚ะฐั†ะธัŽ ะดะปั ะฟะพะปัƒั‡ะตะฝะธั ะดะพะฟะพะปะฝะธั‚ะตะปัŒะฝะพะน ะธะฝั„ะพั€ะผะฐั†ะธะธ. +at_least_one_permission=ะะตะพะฑั…ะพะดะธะผะพ ะฒั‹ะฑั€ะฐั‚ัŒ ั…ะพั‚ั ะฑั‹ ะพะดะฝะพ ั€ะฐะทั€ะตัˆะตะฝะธะต ะดะปั ัะพะทะดะฐะฝะธั ั‚ะพะบะตะฝะฐ +permissions_list=ะ ะฐะทั€ะตัˆะตะฝะธั: manage_oauth2_applications=ะฃะฟั€ะฐะฒะปะตะฝะธะต ะฟั€ะธะปะพะถะตะฝะธัะผะธ OAuth2 edit_oauth2_application=ะ˜ะทะผะตะฝะธั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธะต OAuth2 @@ -957,6 +971,7 @@ mirror_password_blank_placeholder=(ะžั‚ะผะตะฝะตะฝะพ) mirror_password_help=ะกะผะตะฝะธั‚ะต ะธะผั ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะดะปั ัƒะดะฐะปะตะฝะธั ะฟะฐั€ะพะปั. watchers=ะะฐะฑะปัŽะดะฐั‚ะตะปะธ stargazers=ะ—ะฒะตะทะดะพั‡ะตั‚ั‹ +stars_remove_warning=ะ”ะฐะฝะฝะพะต ะดะตะนัั‚ะฒะธะต ัƒะดะฐะปะธั‚ ะฒัะต ะทะฒั‘ะทะดั‹ ะธะท ัั‚ะพะณะพ ั€ะตะฟะพะทะธั‚ะพั€ะธั. forks=ะคะพั€ะบะธ reactions_more=ะธ ะตั‰ั‘ %d unit_disabled=ะะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ ัะฐะนั‚ะฐ ะพั‚ะบะปัŽั‡ะธะป ัั‚ะพั‚ ั€ะฐะทะดะตะป ั€ะตะฟะพะทะธั‚ะพั€ะธั. @@ -1000,6 +1015,7 @@ template.one_item=ะะตะพะฑั…ะพะดะธะผะพ ะฒั‹ะฑั€ะฐั‚ัŒ ั…ะพั‚ั ะฑั‹ ะพะดะธะฝ ั template.invalid=ะะตะพะฑั…ะพะดะธะผะพ ะฒั‹ะฑั€ะฐั‚ัŒ ั…ั€ะฐะฝะธะปะธั‰ะต ัˆะฐะฑะปะพะฝะพะฒ archive.title=ะญั‚ะพ ั€ะตะฟะพะทะธั‚ะพั€ะธะน ะฒ ะฐั€ั…ะธะฒะต. ะ’ั‹ ะผะพะถะตั‚ะต ะตะณะพ ะบะปะพะฝะธั€ะพะฒะฐั‚ัŒ ะธะปะธ ะฟั€ะพัะผะฐั‚ั€ะธะฒะฐั‚ัŒ ั„ะฐะนะปั‹, ะฝะพ ะฝะต ะฒะฝะพัะธั‚ัŒ ะธะทะผะตะฝะตะฝะธั ะธะปะธ ะพั‚ะบั€ั‹ะฒะฐั‚ัŒ ะทะฐะดะฐั‡ะธ/ะทะฐะฟั€ะพัั‹ ะฝะฐ ัะปะธัะฝะธะต. +archive.title_date=ะญั‚ะพั‚ ั€ะตะฟะพะทะธั‚ะพั€ะธะน ะฐั€ั…ะธะฒะธั€ะพะฒะฐะฝ %s. ะ’ั‹ ะผะพะถะตั‚ะต ัะผะพั‚ั€ะตั‚ัŒ ั„ะฐะนะปั‹ ะธะปะธ ะบะปะพะฝะธั€ะพะฒะฐั‚ัŒ ะตะณะพ, ะฝะพ ะฝะต ะผะพะถะตั‚ะต ะฒะฝะพัะธั‚ัŒ ะธะทะผะตะฝะตะฝะธั, ะพั‚ะบั€ั‹ะฒะฐั‚ัŒ ะทะฐะดะฐั‡ะธ ะธะปะธ ะดะตะปะฐั‚ัŒ ะทะฐะฟั€ะพัั‹ ะฝะฐ ัะปะธัะฝะธะต. archive.issue.nocomment=ะญั‚ะพั‚ ั€ะตะฟะพะทะธั‚ะพั€ะธะน ะฒ ะฐั€ั…ะธะฒะต. ะ’ั‹ ะฝะต ะผะพะถะตั‚ะต ะบะพะผะผะตะฝั‚ะธั€ะพะฒะฐั‚ัŒ ะทะฐะดะฐั‡ะธ. archive.pull.nocomment=ะญั‚ะพ ั€ะตะฟะพะทะธั‚ะพั€ะธะน ะฒ ะฐั€ั…ะธะฒะต. ะ’ั‹ ะฝะต ะผะพะถะตั‚ะต ะบะพะผะผะตะฝั‚ะธั€ะพะฒะฐั‚ัŒ ะทะฐะฟั€ะพัั‹ ะฝะฐ ัะปะธัะฝะธะต. @@ -1683,7 +1699,7 @@ pulls.no_merge_access=ะฃ ะฒะฐั ะฝะตั‚ ะฟั€ะฐะฒะฐ ะดะปั ัะปะธัะฝะธั ะดะฐะฝะฝ pulls.merge_pull_request=ะกะพะทะดะฐั‚ัŒ ะบะพะผะผะธั‚ ะฝะฐ ัะปะธัะฝะธะต pulls.rebase_merge_pull_request=ะ’ั‹ะฟะพะปะฝะธั‚ัŒ Rebase, ะฐ ะทะฐั‚ะตะผ fast-forward ัะปะธัะฝะธะต pulls.rebase_merge_commit_pull_request=ะ’ั‹ะฟะพะปะฝะธั‚ัŒ rebase, ะฐ ะทะฐั‚ะตะผ ัะพะทะดะฐั‚ัŒ ะบะพะผะผะธั‚ ัะปะธัะฝะธั -pulls.squash_merge_pull_request=ะกะพะทะดะฐั‚ัŒ ะพะฑัŠะตะดะธะฝะตะฝะฝั‹ะน (squash) ะบะพะผะผะธั‚ +pulls.squash_merge_pull_request=ะกะพะทะดะฐั‚ัŒ ะพะฑัŠะตะดะธะฝั‘ะฝะฝั‹ะน ะบะพะผะผะธั‚ pulls.merge_manually=ะกะปะธั‚ะพ ะฒั€ัƒั‡ะฝัƒัŽ pulls.merge_commit_id=ID ะบะพะผะผะธั‚ะฐ ัะปะธัะฝะธั pulls.require_signed_wont_sign=ะ”ะฐะฝะฝะฐั ะฒะตั‚ะบะฐ ะพะถะธะดะฐะตั‚ ะฟะพะดะฟะธัะฐะฝะฝั‹ะต ะบะพะผะผะธั‚ั‹, ะพะดะฝะฐะบะพ ัะปะธัะฝะธะต ะฝะต ะฑัƒะดะตั‚ ะฟะพะดะฟะธัะฐะฝะพ @@ -1695,6 +1711,7 @@ pulls.rebase_conflict=ะกะปะธัะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ: ะŸั€ะพะธะทะพัˆะตะป ะบ pulls.rebase_conflict_summary=ะกะพะพะฑั‰ะตะฝะธะต ะพะฑ ะพัˆะธะฑะบะต pulls.unrelated_histories=ะกะปะธัะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ: ะฃ ะธัั‚ะพั‡ะฝะธะบะฐ ะธ ั†ะตะปะธ ัะปะธัะฝะธั ะฝะตั‚ ะพะฑั‰ะตะน ะธัั‚ะพั€ะธะธ. ะกะพะฒะตั‚: ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะดั€ัƒะณัƒัŽ ัั‚ั€ะฐั‚ะตะณะธัŽ pulls.merge_out_of_date=ะžัˆะธะฑะบะฐ ัะปะธัะฝะธั: ะฟั€ะธ ัะพะทะดะฐะฝะธะธ ัะปะธัะฝะธั ะฑะฐะทะฐ ะดะฐะฝะฝั‹ั… ะฑั‹ะปะฐ ะพะฑะฝะพะฒะปะตะฝะฐ. ะŸะพะดัะบะฐะทะบะฐ: ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะตั‰ั‘ ั€ะฐะท. +pulls.head_out_of_date=ะžัˆะธะฑะบะฐ ัะปะธัะฝะธั: ะฒะพ ะฒั€ะตะผั ัะปะธัะฝะธั ะณะพะปะพะฒะฝะพะน ะบะพะผะผะธั‚ ะฑั‹ะป ะพะฑะฝะพะฒะปั‘ะฝ. ะŸะพะฟั€ะพะฑัƒะนั‚ะต ะตั‰ั‘ ั€ะฐะท. pulls.push_rejected=ะกะปะธัะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ: ะพั‚ะฟั€ะฐะฒะบะฐ ะฑั‹ะปะฐ ะพั‚ะบะปะพะฝะตะฝะฐ. ะŸั€ะพะฒะตั€ัŒั‚ะต Git-ั…ัƒะบะธ ะดะปั ัั‚ะพะณะพ ั€ะตะฟะพะทะธั‚ะพั€ะธั. pulls.push_rejected_summary=ะŸะพะปะฝะฐั ะพัˆะธะฑะบะฐ ะพั‚ะบะปะพะฝะตะฝะธั pulls.push_rejected_no_message=ะกะปะธัะฝะธะต ะฝะต ัƒะดะฐะปะพััŒ: ะพั‚ะฟั€ะฐะฒะบะฐ ะฑั‹ะปะฐ ะพั‚ะบะปะพะฝะตะฝะฐ, ะฝะพ ัะตั€ะฒะตั€ ะฝะต ัƒะบะฐะทะฐะป ะฟั€ะธั‡ะธะฝัƒ.
ะŸั€ะพะฒะตั€ัŒั‚ะต Git-ั…ัƒะบะธ ะดะปั ัั‚ะพะณะพ ั€ะตะฟะพะทะธั‚ะพั€ะธั @@ -1899,6 +1916,10 @@ settings.githooks=Git-ั…ัƒะบะธ settings.basic_settings=ะžัะฝะพะฒะฝั‹ะต ะฟะฐั€ะฐะผะตั‚ั€ั‹ settings.mirror_settings=ะะฐัั‚ั€ะพะนะบะธ ะทะตั€ะบะฐะปะธั€ะพะฒะฐะฝะธั settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=ะ’ ะฝะฐัั‚ะพัั‰ะตะต ะฒั€ะตะผั ัั‚ะพ ะผะพะถะฝะพ ัะดะตะปะฐั‚ัŒ ั‚ะพะปัŒะบะพ ะฒ ะผะตะฝัŽ ยซะะพะฒะฐั ะผะธะณั€ะฐั†ะธัยป. ะ”ะปั ะฟะพะปัƒั‡ะตะฝะธั ะดะพะฟะพะปะฝะธั‚ะตะปัŒะฝะพะน ะธะฝั„ะพั€ะผะฐั†ะธะธ, ะฟะพะถะฐะปัƒะนัั‚ะฐ, ะพะทะฝะฐะบะพะผัŒั‚ะตััŒ: +settings.mirror_settings.docs.no_new_mirrors=ะ’ะฐัˆ ั€ะตะฟะพะทะธั‚ะพั€ะธะน ะทะตั€ะบะฐะปะธั€ัƒะตั‚ ะธะทะผะตะฝะตะฝะธั ะฒ ะดั€ัƒะณะพะน ั€ะตะฟะพะทะธั‚ะพั€ะธะน ะธะปะธ ะธะท ะฝะตะณะพ. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะธะผะตะนั‚ะต ะฒ ะฒะธะดัƒ, ั‡ั‚ะพ ะฒ ะดะฐะฝะฝั‹ะน ะผะพะผะตะฝั‚ ะฝะตะฒะพะทะผะพะถะฝะพ ัะพะทะดะฐะฒะฐั‚ัŒ ะฝะพะฒั‹ะต ะทะตั€ะบะฐะปะฐ. +settings.mirror_settings.docs.can_still_use=ะฅะพั‚ั ะฒั‹ ะฝะต ะผะพะถะตั‚ะต ะธะทะผะตะฝัั‚ัŒ ััƒั‰ะตัั‚ะฒัƒัŽั‰ะธะต ะทะตั€ะบะฐะปะฐ ะธะปะธ ัะพะทะดะฐะฒะฐั‚ัŒ ะฝะพะฒั‹ะต, ะฒั‹ ะผะพะถะตั‚ะต ะฟะพ-ะฟั€ะตะถะฝะตะผัƒ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ััƒั‰ะตัั‚ะฒัƒัŽั‰ะตะต ะทะตั€ะบะฐะปะพ. +settings.mirror_settings.docs.pull_mirror_instructions=ะงั‚ะพะฑั‹ ะฝะฐัั‚ั€ะพะธั‚ัŒ pull-ะทะตั€ะบะฐะปะพ, ะฟะพะถะฐะปัƒะนัั‚ะฐ, ะพะทะฝะฐะบะพะผัŒั‚ะตััŒ: +settings.mirror_settings.docs.doc_link_title=ะšะฐะบ ะทะตั€ะบะฐะปะธั€ะพะฒะฐั‚ัŒ ั€ะตะฟะพะทะธั‚ะพั€ะธะธ? settings.mirror_settings.docs.pulling_remote_title=ะŸะพะปัƒั‡ะตะฝะธะต ะธะท ัƒะดะฐะปั‘ะฝะฝะพะณะพ ั€ะตะฟะพะทะธั‚ะพั€ะธั settings.mirror_settings.mirrored_repository=ะกะธะฝั…ั€ะพะฝะธะทะธั€ะพะฒะฐะฝะฝะพะต ั…ั€ะฐะฝะธะปะธั‰ะต settings.mirror_settings.direction=ะะฐะฟั€ะฐะฒะปะตะฝะธะต @@ -2012,6 +2033,7 @@ settings.delete_notices_2=- ะญั‚ะฐ ะพะฟะตั€ะฐั†ะธั ะฝะฐะฒัะตะณะดะฐ ัƒะดะฐะปะธ settings.delete_notices_fork_1=- ะ’ัะต ั„ะพั€ะบะธ ัั‚ะฐะฝัƒั‚ ะฝะตะทะฐะฒะธัะธะผั‹ะผะธ ั€ะตะฟะพะทะธั‚ะพั€ะธัะผะธ ะฟะพัะปะต ัƒะดะฐะปะตะฝะธั. settings.deletion_success=ะ ะตะฟะพะทะธั‚ะพั€ะธะน ัƒะดะฐะปั‘ะฝ. settings.update_settings_success=ะะฐัั‚ั€ะพะนะบะธ ั€ะตะฟะพะทะธั‚ะพั€ะธั ะพะฑะฝะพะฒะปะตะฝั‹. +settings.update_settings_no_unit=ะ”ะพะปะถะฝะพ ะฑั‹ั‚ัŒ ั€ะฐะทั€ะตัˆะตะฝะพ ั…ะพั‚ัŒ ะบะฐะบะพะต-ั‚ะพ ะฒะทะฐะธะผะพะดะตะนัั‚ะฒะธะต ั ั€ะตะฟะพะทะธั‚ะพั€ะธะตะผ. settings.confirm_delete=ะฃะดะฐะปะธั‚ัŒ ั€ะตะฟะพะทะธั‚ะพั€ะธะน settings.add_collaborator=ะ”ะพะฑะฐะฒะธั‚ัŒ ัะพะฐะฒั‚ะพั€ะฐ settings.add_collaborator_success=ะกะพะฐะฒั‚ะพั€ ะดะพะฑะฐะฒะปะตะฝ. @@ -2111,6 +2133,7 @@ settings.event_pull_request_sync=ะกะธะฝั…ั€ะพะฝะธะทะฐั†ะธั ะทะฐะฟั€ะพัะฐ ะฝะฐ settings.event_pull_request_sync_desc=ะ—ะฐะฟั€ะพั ะฝะฐ ัะปะธัะฝะธะต ัะธะฝั…ั€ะพะฝะธะทะธั€ะพะฒะฐะฝ. settings.event_pull_request_review_request=ะ—ะฐะฟั€ะพัˆะตะฝะฐ ั€ะตั†ะตะฝะทะธั ะดะปั ะทะฐะฟั€ะพัะฐ ะฝะฐ ัะปะธัะฝะธะต settings.event_pull_request_review_request_desc=ะกะพะทะดะฐะฝ ะธะปะธ ัƒะดะฐะปั‘ะฝ ะทะฐะฟั€ะพั ะฝะฐ ั€ะตั†ะตะฝะทะธัŽ ะดะปั ะทะฐะฟั€ะพัะฐ ะฝะฐ ัะปะธัะฝะธะต. +settings.event_pull_request_approvals=ะฃั‚ะฒะตั€ะถะดะตะฝะธั ะทะฐะฟั€ะพัะพะฒ ะฝะฐ ัะปะธัะฝะธะต settings.event_package=ะŸะฐะบะตั‚ั‹ settings.event_package_desc=ะŸะฐะบะตั‚ ัะพะทะดะฐะฝ ะธะปะธ ัƒะดะฐะปะตะฝ ะฒ ั€ะตะฟะพะทะธั‚ะพั€ะธะธ. settings.branch_filter=ะคะธะปัŒั‚ั€ ะฒะตั‚ะพะบ @@ -2185,8 +2208,11 @@ settings.protect_merge_whitelist_committers_desc=ะ ะฐะทั€ะตัˆะธั‚ัŒ ะฟั€ะธะฝะธะผ settings.protect_merge_whitelist_users=ะŸะพะปัŒะทะพะฒะฐั‚ะตะปะธ ั ะฟั€ะฐะฒะพะผ ะฝะฐ ัะปะธัะฝะธะต: settings.protect_merge_whitelist_teams=ะšะพะผะฐะฝะดั‹, ั‡ะปะตะฝั‹ ะบะพั‚ะพั€ั‹ั… ะพะฑะปะฐะดะฐัŽั‚ ะฟั€ะฐะฒะพะผ ะฝะฐ ัะปะธัะฝะธะต: settings.protect_check_status_contexts=ะ’ะบะปัŽั‡ะธั‚ัŒ ะฟั€ะพะฒะตั€ะบัƒ ัั‚ะฐั‚ัƒัะฐ +settings.protect_status_check_patterns=ะจะฐะฑะปะพะฝั‹ ะฟั€ะพะฒะตั€ะบะธ ัะพัั‚ะพัะฝะธั: settings.protect_check_status_contexts_desc=ะขั€ะตะฑัƒะตั‚ัั ะฟั€ะพะนั‚ะธ ะฟั€ะพะฒะตั€ะบัƒ ัะพัั‚ะพัะฝะธั ะฟะตั€ะตะด ัะปะธัะฝะธะตะผ. ะ’ั‹ะฑะตั€ะธั‚ะต, ะบะฐะบะธะต ะฟั€ะพะฒะตั€ะบะธ ัะพัั‚ะพัะฝะธั ะดะพะปะถะฝั‹ ะฑั‹ั‚ัŒ ะฟั€ะพะนะดะตะฝั‹, ะฟั€ะตะถะดะต ั‡ะตะผ ะฒะตั‚ะฒะธ ะผะพะถะฝะพ ะฑัƒะดะตั‚ ะพะฑัŠะตะดะธะฝะธั‚ัŒ ะฒ ะฒะตั‚ะฒัŒ, ัะพะพั‚ะฒะตั‚ัั‚ะฒัƒัŽั‰ัƒัŽ ัั‚ะพะผัƒ ะฟั€ะฐะฒะธะปัƒ. ะ•ัะปะธ ัั‚ะพั‚ ะฟะฐั€ะฐะผะตั‚ั€ ะฒะบะปัŽั‡ะตะฝ, ะบะพะผะผะธั‚ั‹ ัะฝะฐั‡ะฐะปะฐ ะดะพะปะถะฝั‹ ะฑั‹ั‚ัŒ ะฟะตั€ะตะผะตั‰ะตะฝั‹ ะฒ ะดั€ัƒะณัƒัŽ ะฒะตั‚ะฒัŒ, ะฐ ะทะฐั‚ะตะผ ะพะฑัŠะตะดะธะฝะตะฝั‹ ะธะปะธ ะฟะตั€ะตะผะตั‰ะตะฝั‹ ะฝะตะฟะพัั€ะตะดัั‚ะฒะตะฝะฝะพ ะฒ ะฒะตั‚ะฒัŒ, ัะพะพั‚ะฒะตั‚ัั‚ะฒัƒัŽั‰ัƒัŽ ัั‚ะพะผัƒ ะฟั€ะฐะฒะธะปัƒ, ะฟะพัะปะต ะฟั€ะพั…ะพะถะดะตะฝะธั ะฟั€ะพะฒะตั€ะบะธ ัะพัั‚ะพัะฝะธั. ะ•ัะปะธ ะบะพะฝั‚ะตะบัั‚ั‹ ะฝะต ะฒั‹ะฑั€ะฐะฝั‹, ั‚ะพ ะฟะพัะปะตะดะฝัั ั„ะธะบัะฐั†ะธั ะดะพะปะถะฝะฐ ะฑั‹ั‚ัŒ ัƒัะฟะตัˆะฝะพะน ะฝะตะทะฐะฒะธัะธะผะพ ะพั‚ ะบะพะฝั‚ะตะบัั‚ะฐ. settings.protect_check_status_contexts_list=ะŸั€ะพะฒะตั€ะบะธ ัะพัั‚ะพัะฝะธั ะทะฐ ะฟะพัะปะตะดะฝัŽัŽ ะฝะตะดะตะปัŽ ะดะปั ัั‚ะพะณะพ ั€ะตะฟะพะทะธั‚ะพั€ะธั +settings.protect_invalid_status_check_pattern=ะะตะฒะตั€ะฝั‹ะน ัˆะฐะฑะปะพะฝ ะฟั€ะพะฒะตั€ะบะธ ัะพัั‚ะพัะฝะธั: ยซ%sยป. +settings.protect_no_valid_status_check_patterns=ะะตั‚ ะดะพะฟัƒัั‚ะธะผั‹ั… ัˆะฐะฑะปะพะฝะพะฒ ะฟั€ะพะฒะตั€ะบะธ ัะพัั‚ะพัะฝะธั. settings.protect_required_approvals=ะะตะพะฑั…ะพะดะธะผั‹ะต ะพะดะพะฑั€ะตะฝะธั: settings.protect_required_approvals_desc=ะ ะฐะทั€ะตัˆะธั‚ัŒ ะฟั€ะธะฝัั‚ะธะต ะทะฐะฟั€ะพัะฐ ะฝะฐ ัะปะธัะฝะธะต ั‚ะพะปัŒะบะพ ั ะดะพัั‚ะฐั‚ะพั‡ะฝั‹ะผ ะบะพะปะธั‡ะตัั‚ะฒะพะผ ะฟะพะปะพะถะธั‚ะตะปัŒะฝั‹ั… ะพั‚ะทั‹ะฒะพะฒ. settings.protect_approvals_whitelist_enabled=ะžะณั€ะฐะฝะธั‡ะธั‚ัŒ ัƒั‚ะฒะตั€ะถะดะตะฝะธั ะฑะตะปั‹ะผ ัะฟะธัะบะพะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปะตะน ะธะปะธ ะบะพะผะฐะฝะด @@ -2198,6 +2224,7 @@ settings.dismiss_stale_approvals_desc=ะšะพะณะดะฐ ะฝะพะฒั‹ะต ะบะพะผะผะธั‚ั‹, ะธะท settings.require_signed_commits=ะขั€ะตะฑะพะฒะฐั‚ัŒ ะฟะพะดะฟะธัะฐะฝะฝั‹ะต ะบะพะผะผะธั‚ั‹ settings.require_signed_commits_desc=ะžั‚ะบะปะพะฝะธั‚ัŒ ะพั‚ะฟั€ะฐะฒะบัƒ ะธะทะผะตะฝะตะฝะธะน ะฒ ัั‚ัƒ ะฒะตั‚ะบัƒ, ะตัะปะธ ะพะฝะธ ะฝะต ะฟะพะดะฟะธัะฐะฝั‹ ะธะปะธ ะฝะต ะฟั€ะพะฒะตั€ัะตะผั‹. settings.protect_branch_name_pattern=ะจะฐะฑะปะพะฝ ะธะผะตะฝะธ ะดะปั ะทะฐั‰ะธั‰ั‘ะฝะฝั‹ั… ะฒะตั‚ะพะบ +settings.protect_patterns=ะจะฐะฑะปะพะฝั‹ settings.protect_protected_file_patterns=ะจะฐะฑะปะพะฝั‹ ะทะฐั‰ะธั‰ั‘ะฝะฝั‹ั… ั„ะฐะนะปะพะฒ (ั€ะฐะทะดะตะปั‘ะฝะฝั‹ะต ั‚ะพั‡ะบะพะน ั ะทะฐะฟัั‚ะพะน ';'): settings.protect_protected_file_patterns_desc=ะ—ะฐั‰ะธั‰ะตะฝะฝั‹ะต ั„ะฐะนะปั‹ ะฝะตะปัŒะทั ะธะทะผะตะฝะธั‚ัŒ ะฝะฐะฟั€ัะผัƒัŽ, ะดะฐะถะต ะตัะปะธ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒ ะธะผะตะตั‚ ะฟั€ะฐะฒะพ ะดะพะฑะฐะฒะปัั‚ัŒ, ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะธะปะธ ัƒะดะฐะปัั‚ัŒ ั„ะฐะนะปั‹ ะฒ ัั‚ะพะน ะฒะตั‚ะบะต. ะœะพะถะฝะพ ัƒะบะฐะทะฐั‚ัŒ ะฝะตัะบะพะปัŒะบะพ ัˆะฐะฑะปะพะฝะพะฒ, ั€ะฐะทะดะตะปัั ะธั… ั‚ะพั‡ะบะพะน ั ะทะฐะฟัั‚ะพะน (';'). ะž ัะธะฝั‚ะฐะบัะธัะต ัˆะฐะฑะปะพะฝะพะฒ ั‡ะธั‚ะฐะนั‚ะต ะฒ ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ github.com/gobwas/glob. ะŸั€ะธะผะตั€ั‹: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=ะจะฐะฑะปะพะฝั‹ ะฝะตะทะฐั‰ะธั‰ั‘ะฝะฝั‹ั… ั„ะฐะนะปะพะฒ (ั€ะฐะทะดะตะปั‘ะฝะฝั‹ะต ั‚ะพั‡ะบะพะน ั ะทะฐะฟัั‚ะพะน ';'): @@ -2412,10 +2439,13 @@ branch.protected_deletion_failed=ะ’ะตั‚ะบะฐ ยซ%sยป ะทะฐั‰ะธั‰ะตะฝะฐ. ะ•ั‘ ะฝะตะป branch.default_deletion_failed=ะ’ะตั‚ะบะฐ ยซ%sยป ัะฒะปัะตั‚ัั ะฒะตั‚ะบะพะน ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ. ะ•ั‘ ะฝะตะปัŒะทั ัƒะดะฐะปะธั‚ัŒ. branch.restore=ะ’ะพััั‚ะฐะฝะพะฒะธั‚ัŒ ะฒะตั‚ะบัƒ ยซ%sยป branch.download=ะกะบะฐั‡ะฐั‚ัŒ ะฒะตั‚ะบัƒ ยซ%sยป +branch.rename=ะŸะตั€ะตะธะผะตะฝะพะฒะฐั‚ัŒ ะฒะตั‚ะบัƒ ยซ%sยป branch.included_desc=ะญั‚ะฐ ะฒะตั‚ะบะฐ ัะฒะปัะตั‚ัั ั‡ะฐัั‚ัŒัŽ ะฒะตั‚ะบะธ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ branch.included=ะ’ะบะปัŽั‡ะตะฝะพ branch.create_new_branch=ะกะพะทะดะฐั‚ัŒ ะฒะตั‚ะบัƒ ะธะท ะฒะตั‚ะฒะธ: branch.confirm_create_branch=ะกะพะทะดะฐั‚ัŒ ะฒะตั‚ะบัƒ +branch.warning_rename_default_branch=ะ’ั‹ ะฟะตั€ะตะธะผะตะฝะพะฒั‹ะฒะฐะตั‚ะต ะฒะตั‚ะบัƒ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ. +branch.rename_branch_to=ะŸะตั€ะตะธะผะตะฝะพะฒะฐั‚ัŒ ะฒะตั‚ะบัƒ ยซ%sยป ะฒ: branch.confirm_rename_branch=ะŸะตั€ะตะธะผะตะฝะพะฒะฐั‚ัŒ ะฒะตั‚ะบัƒ branch.create_branch_operation=ะกะพะทะดะฐั‚ัŒ ะฒะตั‚ะบัƒ branch.new_branch=ะกะพะทะดะฐั‚ัŒ ะฝะพะฒัƒัŽ ะฒะตั‚ะบัƒ @@ -2746,6 +2776,7 @@ repos.size=ะ ะฐะทะผะตั€ packages.package_manage_panel=ะฃะฟั€ะฐะฒะปะตะฝะธะต ะฟะฐะบะตั‚ะฐะผะธ packages.total_size=ะžะฑั‰ะธะน ั€ะฐะทะผะตั€: %s +packages.unreferenced_size=ะ ะฐะทะผะตั€ ะฟะพ ััั‹ะปะบะต: %s packages.owner=ะ’ะปะฐะดะตะปะตั† packages.creator=ะะฒั‚ะพั€ packages.name=ะะฐะธะผะตะฝะพะฒะฐะฝะธะต @@ -3012,8 +3043,10 @@ config.git_pull_timeout=ะ›ะธะผะธั‚ ะฒั€ะตะผะตะฝะธ ะฟะพะปัƒั‡ะตะฝะธั ะธะทะผะตะฝ config.git_gc_timeout=ะ›ะธะผะธั‚ ะฒั€ะตะผะตะฝะธ ัะฑะพั€ะบะธ ะผัƒัะพั€ะฐ config.log_config=ะšะพะฝั„ะธะณัƒั€ะฐั†ะธั ะถัƒั€ะฝะฐะปะฐ +config.logger_name_fmt=ะ–ัƒั€ะฝะฐะป: %s config.disabled_logger=ะžั‚ะบะปัŽั‡ะตะฝ config.access_log_mode=ะ ะตะถะธะผ ะดะพัั‚ัƒะฟะฐ ะบ ะถัƒั€ะฝะฐะปัƒ +config.access_log_template=ะจะฐะฑะปะพะฝ ะถัƒั€ะฝะฐะปะฐ ะดะพัั‚ัƒะฟะฐ config.xorm_log_sql=ะ›ะพะณ SQL config.get_setting_failed=ะŸะพะปัƒั‡ะธั‚ัŒ ะฟะฐั€ะฐะผะตั‚ั€ %s ะฝะต ัƒะดะฐะปะพััŒ @@ -3030,6 +3063,7 @@ monitor.execute_times=ะšะพะปะธั‡ะตัั‚ะฒะพ ะฒั‹ะฟะพะปะฝะตะฝะธะน monitor.process=ะ—ะฐะฟัƒั‰ะตะฝะฝั‹ะต ะฟั€ะพั†ะตััั‹ monitor.stacktrace=ะขั€ะฐััะธั€ะพะฒะบะธ ัั‚ะตะบะฐ monitor.processes_count=%d ะฟั€ะพั†ะตััะพะฒ +monitor.download_diagnosis_report=ะกะบะฐั‡ะฐั‚ัŒ ะดะธะฐะณะฝะพัั‚ะธั‡ะตัะบะธะน ะพั‚ั‡ั‘ั‚ monitor.desc=ะžะฟะธัะฐะฝะธะต monitor.start=ะ’ั€ะตะผั ะฝะฐั‡ะฐะปะฐ monitor.execute_time=ะ’ั€ะตะผั ะฒั‹ะฟะพะปะฝะตะฝะธั @@ -3050,9 +3084,10 @@ monitor.queue.numberinqueue=ะŸะพะทะธั†ะธั ะฒ ะพั‡ะตั€ะตะดะธ monitor.queue.review=ะŸั€ะพัะผะพั‚ั€ ะบะพะฝั„ะธะณัƒั€ะฐั†ะธะธ monitor.queue.review_add=ะŸั€ะพัะผะพั‚ั€ะตั‚ัŒ/ะดะพะฑะฐะฒะธั‚ัŒ ั€ะฐะฑะพั‡ะธั… monitor.queue.settings.title=ะะฐัั‚ั€ะพะนะบะธ ะฟัƒะปะฐ +monitor.queue.settings.desc=ะŸัƒะปั‹ ัƒะฒะตะปะธั‡ะธะฒะฐัŽั‚ัั ะดะธะฝะฐะผะธั‡ะตัะบะธ ะฒ ะพั‚ะฒะตั‚ ะฝะฐ ะฑะปะพะบะธั€ะพะฒะบัƒ ะพั‡ะตั€ะตะดะตะน ัะฒะพะธั… ั€ะฐะฑะพั‡ะธั…. monitor.queue.settings.maxnumberworkers=ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ั€ะฐะฑะพั‡ะธั… monitor.queue.settings.maxnumberworkers.placeholder=ะ’ ะฝะฐัั‚ะพัั‰ะตะต ะฒั€ะตะผั %[1]d -monitor.queue.settings.maxnumberworkers.error=ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ั€ะฐะฑะพั‚ะฝะธะบะพะฒ ะดะพะปะถะฝะพ ะฑั‹ั‚ัŒ ั‡ะธัะปะพะผ +monitor.queue.settings.maxnumberworkers.error=ะœะฐะบัะธะผะฐะปัŒะฝะพะต ะบะพะปะธั‡ะตัั‚ะฒะพ ั€ะฐะฑะพั‡ะธั… ะดะพะปะถะฝะพ ะฑั‹ั‚ัŒ ั‡ะธัะปะพะผ monitor.queue.settings.submit=ะžะฑะฝะพะฒะธั‚ัŒ ะฝะฐัั‚ั€ะพะนะบะธ monitor.queue.settings.changed=ะะฐัั‚ั€ะพะนะบะธ ะพะฑะฝะพะฒะปะตะฝั‹ monitor.queue.settings.remove_all_items=ะฃะดะฐะปะธั‚ัŒ ะฒัะต @@ -3166,7 +3201,7 @@ error.unit_not_allowed=ะฃ ะฒะฐั ะฝะตั‚ ะดะพัั‚ัƒะฟะฐ ะบ ัั‚ะพะผัƒ ั€ะฐะทะดะต title=ะŸะฐะบะตั‚ั‹ desc=ะฃะฟั€ะฐะฒะปะตะฝะธะต ะฟะฐะบะตั‚ะฐะผะธ ั€ะตะฟะพะทะธั‚ะพั€ะธั. empty=ะŸะพะบะฐ ะฝะตั‚ ะฟะฐะบะตั‚ะพะฒ. -empty.documentation=ะ”ะพะฟะพะปะฝะธั‚ะตะปัŒะฝัƒัŽ ะธะฝั„ะพั€ะผะฐั†ะธัŽ ะพ ั€ะตะตัั‚ั€ะต ะฟะฐะบะตั‚ะพะฒ ะผะพะถะฝะพ ะฝะฐะนั‚ะธ ะฒ ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ. +empty.documentation=ะ”ะพะฟะพะปะฝะธั‚ะตะปัŒะฝัƒัŽ ะธะฝั„ะพั€ะผะฐั†ะธัŽ ะพ ั€ะตะตัั‚ั€ะต ะฟะฐะบะตั‚ะพะฒ ะผะพะถะฝะพ ะฝะฐะนั‚ะธ ะฒ ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ. empty.repo=ะ’ั‹ ะทะฐะณั€ัƒะทะธะปะธ ะฟะฐะบะตั‚, ะฝะพ ะพะฝ ะทะดะตััŒ ะฝะต ะพั‚ะพะฑั€ะฐะถะฐะตั‚ัั? ะŸะตั€ะตะนะดะธั‚ะต ะฒ ะฝะฐัั‚ั€ะพะนะบะธ ะฟะฐะบะตั‚ะฐ ะธ ัะฒัะถะธั‚ะต ะตะณะพ ั ัั‚ะธะผ ั€ะตะฟะพะทะธั‚ะพั€ะธะตะผ. filter.type=ะขะธะฟ filter.type.all=ะ’ัะต diff --git a/package-lock.json b/package-lock.json index 173f42b30fa7..437534b525f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,11 @@ "@github/relative-time-element": "4.3.0", "@github/text-expander-element": "2.5.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.3.0", + "@primer/octicons": "19.4.0", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", - "ansi-to-html": "0.7.2", - "asciinema-player": "3.4.0", + "ansi_up": "5.2.1", + "asciinema-player": "3.5.0", "clippie": "4.0.1", "css-loader": "6.8.1", "dropzone": "6.0.0-beta.2", @@ -28,26 +28,27 @@ "fast-glob": "3.2.12", "jquery": "3.7.0", "jquery.are-you-sure": "1.9.0", - "katex": "0.16.7", + "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "mermaid": "10.2.3", "mini-css-extract-plugin": "2.7.6", - "minimatch": "9.0.1", + "minimatch": "9.0.2", "monaco-editor": "0.39.0", "monaco-editor-webpack-plugin": "7.0.1", "pdfobject": "2.2.12", "pretty-ms": "8.0.0", "sortablejs": "1.15.0", - "swagger-ui-dist": "5.0.0", + "swagger-ui-dist": "5.1.0", "throttle-debounce": "5.0.0", "tippy.js": "6.3.7", + "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", "vue": "3.3.4", "vue-bar-graph": "2.0.0", "vue-loader": "17.2.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.87.0", + "webpack": "5.88.0", "webpack-cli": "5.1.4", "wrap-ansi": "8.1.0" }, @@ -67,17 +68,17 @@ "eslint-plugin-regexp": "1.15.0", "eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-unicorn": "47.0.0", - "eslint-plugin-vue": "9.14.1", + "eslint-plugin-vue": "9.15.1", "eslint-plugin-wc": "1.5.0", "jsdom": "22.1.0", "markdownlint-cli": "0.35.0", "postcss-html": "1.5.0", - "stylelint": "15.8.0", + "stylelint": "15.9.0", "stylelint-declaration-block-no-ignored-properties": "2.7.0", "stylelint-declaration-strict-value": "1.9.2", "stylelint-stylistic": "0.4.2", "svgo": "3.0.2", - "updates": "14.2.4", + "updates": "14.2.8", "vitest": "0.32.2" }, "engines": { @@ -402,9 +403,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.0.tgz", - "integrity": "sha512-MXkR+TeaS2q9IkpyO6jVCdtA/bfpABJxIrfkLswThFN8EZZgI2RfAHhm6sDNDuYV25d5+b8Lj1fpTccIcSLPsQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.1.tgz", + "integrity": "sha512-pUjtFbaKbiFNjJo8pprrIaXLvQvWIlwPiFnRI4sEnc4F0NIGTOsw8kaJSR3CmZAKEvV8QYckovgAnWQC0bgLLQ==", "dev": true, "funding": [ { @@ -420,7 +421,7 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-parser-algorithms": "^2.2.0", "@csstools/css-tokenizer": "^2.1.1" } }, @@ -1235,9 +1236,9 @@ } }, "node_modules/@primer/octicons": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.3.0.tgz", - "integrity": "sha512-hyIo54VPC3VI7ZyAgosiJcbhxq1gZLbBspZwN9cg1uImRd2E8T9JST3kGeezezJYPjG367FuF7p1L+gmLmeESw==", + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.4.0.tgz", + "integrity": "sha512-92eXALm3ucZkzqpJmJbC+fR9ldiuNd4W4s2MZQNQIBahpg14emJ+I9fdHqCummFlfgyohLzXn++7rz0NlkqAJA==", "dependencies": { "object-assign": "^4.1.1" } @@ -1858,9 +1859,9 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + "version": "20.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz", + "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -2465,6 +2466,14 @@ "ajv": "^8.8.2" } }, + "node_modules/ansi_up": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-5.2.1.tgz", + "integrity": "sha512-5bz5T/7FRmlxA37zDXhG6cAwlcZtfnmNLDJra66EEIT3kYlw5aPJdbkJEhm59D6kA4Wi5ict6u6IDYHJaQlH+g==", + "engines": { + "node": "*" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2487,20 +2496,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansi-to-html": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", - "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", - "dependencies": { - "entities": "^2.2.0" - }, - "bin": { - "ansi-to-html": "bin/ansi-to-html" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2610,9 +2605,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.4.0.tgz", - "integrity": "sha512-dX6jt5S3K6daItsVWzyY9mRDK+ivC2QgqCxFkdSiNslo0vY/ZqA4upcTzqIKZqBtxppovOZk44ltg9VnHG9QVg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.5.0.tgz", + "integrity": "sha512-o4B2AscBuCZo4+JB9TBGrfZ7GQL99wsbm08WwmuNJTPd1lyLQJq8wgacnBsdvb2sC0K875ScYr8T5XmfeH/6dg==", "dependencies": { "@babel/runtime": "^7.21.0", "solid-js": "^1.3.0" @@ -2882,9 +2877,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001504", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001504.tgz", - "integrity": "sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==", + "version": "1.0.30001508", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001508.tgz", + "integrity": "sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw==", "funding": [ { "type": "opencollective", @@ -4167,9 +4162,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.433", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.433.tgz", - "integrity": "sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==" + "version": "1.4.441", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.441.tgz", + "integrity": "sha512-LlCgQ8zgYZPymf5H4aE9itwiIWH4YlCiv1HFLmmcBeFYi5E+3eaIFnjHzYtcFQbaKfAW+CqZ9pgxo33DZuoqPg==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -4211,17 +4206,21 @@ } }, "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", "bin": { "envinfo": "dist/cli.js" }, @@ -4820,9 +4819,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.14.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz", - "integrity": "sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==", + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.15.1.tgz", + "integrity": "sha512-CJE/oZOslvmAR9hf8SClTdQ9JLweghT6JCBQNrT2Iel1uVw0W0OLJxzvPd6CxmABKCvLrtyDnqGV37O7KQv6+A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.3.0", @@ -5378,9 +5377,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.0.tgz", - "integrity": "sha512-lgbo68hHTQnFddybKbbs/RDRJnJT5YyGy2kQzVwbq+g67X73i+5MVTval34QxGkOe9X5Ujf1UYpCaphLyltjEg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz", + "integrity": "sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -6590,9 +6589,9 @@ "integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==" }, "node_modules/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-ftuDnJbcbOckGY11OO+zg3OofESlbR5DRl2cmN8HeWeeFIV7wTXvAOx8kEjZjobhA+9wh2fbKeO6cdcA9Mnovg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -6895,18 +6894,6 @@ "markdown-it": "bin/markdown-it.js" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/markdownlint": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.29.0.tgz", @@ -7669,9 +7656,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz", + "integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7715,13 +7702,13 @@ } }, "node_modules/mlly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz", - "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", + "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", "dev": true, "dependencies": { - "acorn": "^8.8.2", - "pathe": "^1.1.0", + "acorn": "^8.9.0", + "pathe": "^1.1.1", "pkg-types": "^1.0.3", "ufo": "^1.1.2" } @@ -9076,14 +9063,14 @@ "dev": true }, "node_modules/run-con": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.11.tgz", - "integrity": "sha512-NEMGsUT+cglWkzEr4IFK21P4Jca45HqiAbIIZIBdX5+UZTB24Mb/21iNGgz9xZa8tL6vbW7CXmq7MFN42+VjNQ==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.12.tgz", + "integrity": "sha512-5257ILMYIF4RztL9uoZ7V9Q97zHtNHn5bN3NobeAnzB1P3ASLgg8qocM2u+R18ttp+VEM78N2LK8XcNVtnSRrg==", "dev": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~3.0.0", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "strip-json-comments": "~3.1.1" }, "bin": { @@ -9223,9 +9210,9 @@ } }, "node_modules/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9428,9 +9415,9 @@ "dev": true }, "node_modules/solid-js": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.6.tgz", - "integrity": "sha512-DXVOTjUh/bIAhE0fIqu3ezGLyQaez7v8EOw3uPLIi87DmLjg+hsuCAgKyNIZ+o4jUetOk3ZORccvJmE1yZUk8g==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.7.tgz", + "integrity": "sha512-SPdYVke/Z6Za24PBTbULyQYPrhGO1ZbPany76atO2zF2dmYn2pCotbsw1JtlgWnr9dK2JbwPGnA3ODTGPLhZNw==", "dependencies": { "csstype": "^3.1.0", "seroval": "^0.5.0" @@ -9726,9 +9713,9 @@ "dev": true }, "node_modules/stylelint": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.8.0.tgz", - "integrity": "sha512-x9qBk84F3MEjMEUNCE7MtWmfj9G9y5XzJ0cpQeJdy2l/IoqjC8Ih0N0ytmOTnXE4Yv0J7I1cmVRQUVNSPCxTsA==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.9.0.tgz", + "integrity": "sha512-sXtAZi64CllWr6A+8ymDWnlIaYwuAa7XRmGnJxLQXFNnLjd3Izm4HAD+loKVaZ7cpK6SLxhAUX1lwPJKGCn0mg==", "dev": true, "dependencies": { "@csstools/css-parser-algorithms": "^2.2.0", @@ -9840,9 +9827,9 @@ } }, "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" }, "node_modules/superstruct": { "version": "0.10.13", @@ -9924,9 +9911,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.0.0.tgz", - "integrity": "sha512-bwl6og9I9CAHKGSnYLKydjhBuH7d3oU6RX6uKN8oDCkLusTHXOW3sZMyBWjRtjGFnCMmN085oZoaR/4Wm9nIaQ==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.1.0.tgz", + "integrity": "sha512-c1KmAjuVODxw+vwkNLALQZrgdlBAuBbr2xSPfYrJgseEi7gFKcTvShysPmyuDI4kcUa1+5rFpjWvXdusKY74mg==" }, "node_modules/symbol-tree": { "version": "3.2.4", @@ -9971,9 +9958,9 @@ } }, "node_modules/terser": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.0.tgz", - "integrity": "sha512-pdL757Ig5a0I+owA42l6tIuEycRuM7FPY4n62h44mRLRfnOxJkkOHd6i89dOpwZlpF6JXBwaAHF6yWzFrt+QyA==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz", + "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -10136,6 +10123,11 @@ "node": ">=8.0" } }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -10228,9 +10220,9 @@ } }, "node_modules/tslib": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", - "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", "dev": true }, "node_modules/type-check": { @@ -10377,9 +10369,9 @@ } }, "node_modules/updates": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/updates/-/updates-14.2.4.tgz", - "integrity": "sha512-r54h4Q12lUAmQ9dENy7BnY22AnTfW4YGEZw73gv6RvNEWgcZ3qS88jPLc1ckPAzt/8TPKWwLkSVpbEpgGwglJw==", + "version": "14.2.8", + "resolved": "https://registry.npmjs.org/updates/-/updates-14.2.8.tgz", + "integrity": "sha512-Ca+M1vKKBBRiQSi3yrN8OdncmP9osIf1oJM/HpEIHeDvyGLs/noTi9X2LS4zl50VXRTSCqssF5CZN0XWzSPigg==", "dev": true, "bin": { "updates": "bin/updates.js" @@ -10542,9 +10534,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", - "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz", + "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -10780,9 +10772,9 @@ } }, "node_modules/webpack": { - "version": "5.87.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.87.0.tgz", - "integrity": "sha512-GOu1tNbQ7p1bDEoFRs2YPcfyGs8xq52yyPBZ3m2VGnXGtV9MxjrkABHm4V9Ia280OefsSLzvbVoXcfLxjKY/Iw==", + "version": "5.88.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.0.tgz", + "integrity": "sha512-O3jDhG5e44qIBSi/P6KpcCcH7HD+nYIHVBhdWFxcLOcIGN8zGo5nqF3BjyNCxIh4p1vFdNnreZv2h2KkoAw3lw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", diff --git a/package.json b/package.json index 0701862cc2a6..da95145e4abb 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,11 @@ "@github/relative-time-element": "4.3.0", "@github/text-expander-element": "2.5.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.3.0", + "@primer/octicons": "19.4.0", "@webcomponents/custom-elements": "1.6.0", "add-asset-webpack-plugin": "2.0.1", - "ansi-to-html": "0.7.2", - "asciinema-player": "3.4.0", + "ansi_up": "5.2.1", + "asciinema-player": "3.5.0", "clippie": "4.0.1", "css-loader": "6.8.1", "dropzone": "6.0.0-beta.2", @@ -27,26 +27,27 @@ "fast-glob": "3.2.12", "jquery": "3.7.0", "jquery.are-you-sure": "1.9.0", - "katex": "0.16.7", + "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "mermaid": "10.2.3", "mini-css-extract-plugin": "2.7.6", - "minimatch": "9.0.1", + "minimatch": "9.0.2", "monaco-editor": "0.39.0", "monaco-editor-webpack-plugin": "7.0.1", "pdfobject": "2.2.12", "pretty-ms": "8.0.0", "sortablejs": "1.15.0", - "swagger-ui-dist": "5.0.0", + "swagger-ui-dist": "5.1.0", "throttle-debounce": "5.0.0", "tippy.js": "6.3.7", + "toastify-js": "1.12.0", "tributejs": "5.1.3", "uint8-to-base64": "0.2.0", "vue": "3.3.4", "vue-bar-graph": "2.0.0", "vue-loader": "17.2.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.87.0", + "webpack": "5.88.0", "webpack-cli": "5.1.4", "wrap-ansi": "8.1.0" }, @@ -66,17 +67,17 @@ "eslint-plugin-regexp": "1.15.0", "eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-unicorn": "47.0.0", - "eslint-plugin-vue": "9.14.1", + "eslint-plugin-vue": "9.15.1", "eslint-plugin-wc": "1.5.0", "jsdom": "22.1.0", "markdownlint-cli": "0.35.0", "postcss-html": "1.5.0", - "stylelint": "15.8.0", + "stylelint": "15.9.0", "stylelint-declaration-block-no-ignored-properties": "2.7.0", "stylelint-declaration-strict-value": "1.9.2", "stylelint-stylistic": "0.4.2", "svgo": "3.0.2", - "updates": "14.2.4", + "updates": "14.2.8", "vitest": "0.32.2" }, "browserslist": [ diff --git a/poetry.lock b/poetry.lock index 69258f749c99..7d106e15519e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -42,13 +42,13 @@ six = ">=1.13.0" [[package]] name = "djlint" -version = "1.31.0" +version = "1.31.1" description = "HTML Template Linter and Formatter" optional = false python-versions = ">=3.8.0,<4.0.0" files = [ - {file = "djlint-1.31.0-py3-none-any.whl", hash = "sha256:2b9200c67103b79835b7547ff732e910888d1f0ef684f5b329eb64b14d09c046"}, - {file = "djlint-1.31.0.tar.gz", hash = "sha256:8acb4b751b429c5aabb1aef5b6007bdf53224eceda25c5fbe04c42cc57c0a7ba"}, + {file = "djlint-1.31.1-py3-none-any.whl", hash = "sha256:9b2e2fc3a059a8e5a62f309edea15c1aeee331a279ab2699b9fb51a31d8c0934"}, + {file = "djlint-1.31.1.tar.gz", hash = "sha256:a11739e2f919f760b3986eb13d06e00171f3bd342b8d88e9bd914a4260eaa8ce"}, ] [package.dependencies] @@ -328,4 +328,4 @@ telegram = ["requests"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "22c4af11eadd8784b613951d6160d67be0f33500238a450741c3d75beb218dad" +content-hash = "f03ad8e7c4f6e797ac3c04630db8cc16438cd59642653c26fd401633cd62d696" diff --git a/public/img/svg/octicon-copilot-error.svg b/public/img/svg/octicon-copilot-error.svg index a0e2232f737c..caaf0d5ec3da 100644 --- a/public/img/svg/octicon-copilot-error.svg +++ b/public/img/svg/octicon-copilot-error.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/img/svg/octicon-copilot-warning.svg b/public/img/svg/octicon-copilot-warning.svg index a9d856384d1f..ce956452046e 100644 --- a/public/img/svg/octicon-copilot-warning.svg +++ b/public/img/svg/octicon-copilot-warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/img/svg/octicon-file-directory-symlink.svg b/public/img/svg/octicon-file-directory-symlink.svg new file mode 100644 index 000000000000..ddc2e3fd6705 --- /dev/null +++ b/public/img/svg/octicon-file-directory-symlink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ce5f475b272f..7a30f5914066 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [] python = "^3.8" [tool.poetry.group.dev.dependencies] -djlint = "1.31.0" +djlint = "1.31.1" [tool.djlint] profile="golang" diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index cc9c06ab455c..3370355f15c7 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -9,6 +9,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" secret_model "code.gitea.io/gitea/models/secret" + actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -54,8 +55,10 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { secrets := map[string]string{} - if task.Job.Run.IsForkPullRequest { + if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget { // ignore secrets for fork pull request + // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch + // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target return secrets } @@ -116,6 +119,14 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { event := map[string]interface{}{} _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) + // TriggerEvent is added in https://github.com/go-gitea/gitea/pull/25229 + // This fallback is for the old ActionRun that doesn't have the TriggerEvent field + // and should be removed in 1.22 + eventName := t.Job.Run.TriggerEvent + if eventName == "" { + eventName = t.Job.Run.Event.Event() + } + baseRef := "" headRef := "" if pullPayload, err := t.Job.Run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { @@ -137,7 +148,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. - "event_name": t.Job.Run.Event.Event(), // string, The name of the event that triggered the workflow run. + "event_name": eventName, // string, The name of the event that triggered the workflow run. "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. "graphql_url": "", // string, The URL of the GitHub GraphQL API. "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. @@ -163,7 +174,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. // additional contexts - "gitea_default_actions_url": setting.Actions.DefaultActionsURL, + "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), }) if err != nil { log.Error("structpb.NewStruct failed: %v", err) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index be66cc524081..0e28bde68309 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -777,11 +777,11 @@ func Routes() *web.Route { m.Group("/notifications", func() { m.Combo(""). Get(notify.ListNotifications). - Put(notify.ReadNotifications, reqToken()) + Put(reqToken(), notify.ReadNotifications) m.Get("/new", notify.NewAvailable) m.Combo("/threads/{id}"). Get(notify.GetThread). - Patch(notify.ReadThread, reqToken()) + Patch(reqToken(), notify.ReadThread) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) // Users (requires user scope) @@ -899,6 +899,11 @@ func Routes() *web.Route { Patch(bind(api.EditHookOption{}), user.EditHook). Delete(user.DeleteHook) }, reqWebhooksEnabled()) + + m.Group("/avatar", func() { + m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar) + m.Delete("", user.DeleteAvatar) + }, reqToken()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) @@ -1134,6 +1139,10 @@ func Routes() *web.Route { m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) m.Get("/activities/feeds", repo.ListRepoActivityFeeds) m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed) + m.Group("/avatar", func() { + m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar) + m.Delete("", repo.DeleteAvatar) + }, reqAdmin(), reqToken()) }, repoAssignment()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) @@ -1314,6 +1323,10 @@ func Routes() *web.Route { Patch(bind(api.EditHookOption{}), org.EditHook). Delete(org.DeleteHook) }, reqToken(), reqOrgOwnership(), reqWebhooksEnabled()) + m.Group("/avatar", func() { + m.Post("", bind(api.UpdateUserAvatarOption{}), org.UpdateAvatar) + m.Delete("", org.DeleteAvatar) + }, reqToken(), reqOrgOwnership()) m.Get("/activities/feeds", org.ListOrgActivityFeeds) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) m.Group("/teams/{teamid}", func() { diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index bd3b86a6f152..e16c54a2c0bb 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -183,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.InternalServerError(err) + ctx.Error(http.StatusBadRequest, "Parse", err) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 2261610c0923..a9c6b4361794 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -132,7 +132,7 @@ func ReadNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.InternalServerError(err) + ctx.Error(http.StatusBadRequest, "Parse", err) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go new file mode 100644 index 000000000000..b3cb0b81a6be --- /dev/null +++ b/routers/api/v1/org/avatar.go @@ -0,0 +1,74 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "encoding/base64" + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + user_service "code.gitea.io/gitea/services/user" +) + +// UpdateAvatarupdates the Avatar of an Organisation +func UpdateAvatar(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/avatar organization orgUpdateAvatar + // --- + // summary: Update Avatar + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateUserAvatarOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + form := web.GetForm(ctx).(*api.UpdateUserAvatarOption) + + content, err := base64.StdEncoding.DecodeString(form.Image) + if err != nil { + ctx.Error(http.StatusBadRequest, "DecodeImage", err) + return + } + + err = user_service.UploadAvatar(ctx.Org.Organization.AsUser(), content) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UploadAvatar", err) + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteAvatar deletes the Avatar of an Organisation +func DeleteAvatar(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/avatar organization orgDeleteAvatar + // --- + // summary: Delete Avatar + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + err := user_service.DeleteAvatar(ctx.Org.Organization.AsUser()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err) + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 8b7d7b195151..1aaefacdd596 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -561,12 +561,12 @@ func GetTeamRepos(ctx *context.APIContext) { } repos := make([]*api.Repository, len(teamRepos)) for i, repo := range teamRepos { - access, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) return } - repos[i] = convert.ToRepo(ctx, repo, access) + repos[i] = convert.ToRepo(ctx, repo, permission) } ctx.SetTotalCountHeader(int64(team.NumRepos)) ctx.JSON(http.StatusOK, repos) @@ -612,13 +612,13 @@ func GetTeamRepo(ctx *context.APIContext) { return } - access, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, access)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) } // getRepositoryByParams get repository by a team's organization ID and repo name diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go new file mode 100644 index 000000000000..48bd143d0c8a --- /dev/null +++ b/routers/api/v1/repo/avatar.go @@ -0,0 +1,84 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "encoding/base64" + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + repo_service "code.gitea.io/gitea/services/repository" +) + +// UpdateVatar updates the Avatar of an Repo +func UpdateAvatar(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/avatar repository repoUpdateAvatar + // --- + // summary: Update avatar + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateRepoAvatarOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + form := web.GetForm(ctx).(*api.UpdateRepoAvatarOption) + + content, err := base64.StdEncoding.DecodeString(form.Image) + if err != nil { + ctx.Error(http.StatusBadRequest, "DecodeImage", err) + return + } + + err = repo_service.UploadAvatar(ctx, ctx.Repo.Repository, content) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UploadAvatar", err) + } + + ctx.Status(http.StatusNoContent) +} + +// UpdateAvatar deletes the Avatar of an Repo +func DeleteAvatar(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/avatar repository repoDeleteAvatar + // --- + // summary: Delete avatar + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository) + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err) + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 5336ccb79743..5e2c9878f088 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -15,7 +15,9 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" + repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/convert" @@ -76,7 +78,7 @@ func GetBranch(ctx *context.APIContext) { return } - br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) if err != nil { ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) return @@ -116,8 +118,54 @@ func DeleteBranch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" + if ctx.Repo.Repository.IsEmpty { + ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + return + } + + if ctx.Repo.Repository.IsArchived { + ctx.Error(http.StatusForbidden, "", "Git Repository is archived.") + return + } + + if ctx.Repo.Repository.IsMirror { + ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + return + } + branchName := ctx.Params("*") + if ctx.Repo.Repository.IsEmpty { + ctx.Error(http.StatusForbidden, "", "Git Repository is empty.") + return + } + + // check whether branches of this repository has been synced + totalNumOfBranches, err := git_model.CountBranches(ctx, git_model.FindBranchOptions{ + RepoID: ctx.Repo.Repository.ID, + IsDeletedBranch: util.OptionalBoolFalse, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CountBranches", err) + return + } + if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch + _, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) + if err != nil { + ctx.ServerError("SyncRepoBranches", err) + return + } + } + + if ctx.Repo.Repository.IsArchived { + ctx.Error(http.StatusForbidden, "IsArchived", fmt.Errorf("can not delete branch of an archived repository")) + return + } + if ctx.Repo.Repository.IsMirror { + ctx.Error(http.StatusForbidden, "IsMirrored", fmt.Errorf("can not delete branch of an mirror repository")) + return + } + if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { switch { case git.IsErrBranchNotExist(err): @@ -162,17 +210,30 @@ func CreateBranch(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/Branch" + // "403": + // description: The branch is archived or a mirror. // "404": // description: The old branch does not exist. // "409": // description: The branch with the same name already exists. - opt := web.GetForm(ctx).(*api.CreateBranchRepoOption) if ctx.Repo.Repository.IsEmpty { ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") return } + if ctx.Repo.Repository.IsArchived { + ctx.Error(http.StatusForbidden, "", "Git Repository is archived.") + return + } + + if ctx.Repo.Repository.IsMirror { + ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + return + } + + opt := web.GetForm(ctx).(*api.CreateBranchRepoOption) + var oldCommit *git.Commit var err error @@ -203,14 +264,14 @@ func CreateBranch(ctx *context.APIContext) { err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName) if err != nil { - if models.IsErrBranchDoesNotExist(err) { + if git_model.IsErrBranchNotExist(err) { ctx.Error(http.StatusNotFound, "", "The old branch does not exist") } if models.IsErrTagAlreadyExists(err) { ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.") - } else if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { + } else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { ctx.Error(http.StatusConflict, "", "The branch already exists.") - } else if models.IsErrBranchNameConflict(err) { + } else if git_model.IsErrBranchNameConflict(err) { ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.") } else { ctx.Error(http.StatusInternalServerError, "CreateNewBranchFromCommit", err) @@ -236,7 +297,7 @@ func CreateBranch(ctx *context.APIContext) { return } - br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) if err != nil { ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) return @@ -275,20 +336,43 @@ func ListBranches(ctx *context.APIContext) { // "200": // "$ref": "#/responses/BranchList" - var totalNumOfBranches int + var totalNumOfBranches int64 var apiBranches []*api.Branch listOptions := utils.GetListOptions(ctx) - if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil { + if !ctx.Repo.Repository.IsEmpty { + if ctx.Repo.GitRepo == nil { + ctx.Error(http.StatusInternalServerError, "Load git repository failed", nil) + return + } + + branchOpts := git_model.FindBranchOptions{ + ListOptions: listOptions, + RepoID: ctx.Repo.Repository.ID, + IsDeletedBranch: util.OptionalBoolFalse, + } + var err error + totalNumOfBranches, err = git_model.CountBranches(ctx, branchOpts) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CountBranches", err) + return + } + if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch + totalNumOfBranches, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) + if err != nil { + ctx.ServerError("SyncRepoBranches", err) + return + } + } + rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err) return } - skip, _ := listOptions.GetStartEnd() - branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize) + branches, err := git_model.FindBranches(ctx, branchOpts) if err != nil { ctx.Error(http.StatusInternalServerError, "GetBranches", err) return @@ -296,11 +380,11 @@ func ListBranches(ctx *context.APIContext) { apiBranches = make([]*api.Branch, 0, len(branches)) for i := range branches { - c, err := branches[i].GetCommit() + c, err := ctx.Repo.GitRepo.GetBranchCommit(branches[i].Name) if err != nil { // Skip if this branch doesn't exist anymore. if git.IsErrNotExist(err) { - total-- + totalNumOfBranches-- continue } ctx.Error(http.StatusInternalServerError, "GetCommit", err) @@ -308,19 +392,17 @@ func ListBranches(ctx *context.APIContext) { } branchProtection := rules.GetFirstMatched(branches[i].Name) - apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, 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.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize) + ctx.SetTotalCountHeader(totalNumOfBranches) ctx.JSON(http.StatusOK, apiBranches) } @@ -580,7 +662,7 @@ func CreateBranchProtection(ctx *context.APIContext) { }() } // FIXME: since we only need to recheck files protected rules, we could improve this - matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, ruleName) + matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName) if err != nil { ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) return @@ -851,7 +933,7 @@ func EditBranchProtection(ctx *context.APIContext) { } // FIXME: since we only need to recheck files protected rules, we could improve this - matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName) + matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName) if err != nil { ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) return diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 2b468d6e739b..48f890ee552b 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -687,12 +687,12 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { ctx.Error(http.StatusForbidden, "Access", err) return } - if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || + if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) return } - if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) { + if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) return } @@ -843,7 +843,7 @@ func DeleteFile(ctx *context.APIContext) { if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { ctx.Error(http.StatusNotFound, "DeleteFile", err) return - } else if models.IsErrBranchAlreadyExists(err) || + } else if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || models.IsErrCommitIDDoesNotMatch(err) || diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 4cb3eddf58b0..f75153ab2d89 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -60,12 +60,12 @@ func ListForks(ctx *context.APIContext) { } apiForks := make([]*api.Repository, len(forks)) for i, fork := range forks { - access, err := access_model.AccessLevel(ctx, ctx.Doer, fork) + permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return } - apiForks[i] = convert.ToRepo(ctx, fork, access) + apiForks[i] = convert.ToRepo(ctx, fork, permission) } ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks)) @@ -152,5 +152,5 @@ func CreateFork(ctx *context.APIContext) { } // TODO change back to 201 - ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, fork, perm.AccessModeOwner)) + ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, fork, access_model.Permission{AccessMode: perm.AccessModeOwner})) } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 39d83912b016..d0b77b5687ed 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -8,6 +8,7 @@ import ( "net/http" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -185,7 +186,7 @@ func TestHook(ctx *context.APIContext) { Commits: []*api.PayloadCommit{commit}, TotalCommits: 1, HeadCommit: commit, - Repo: convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeNone), + Repo: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}), Pusher: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), }); err != nil { diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index d496c4a73c11..824880880a01 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -13,6 +13,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" @@ -27,13 +28,13 @@ import ( func appendPrivateInformation(ctx stdCtx.Context, apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) { apiKey.ReadOnly = key.Mode == perm.AccessModeRead if repository.ID == key.RepoID { - apiKey.Repository = convert.ToRepo(ctx, repository, key.Mode) + apiKey.Repository = convert.ToRepo(ctx, repository, access_model.Permission{AccessMode: key.Mode}) } else { repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID) if err != nil { return apiKey, err } - apiKey.Repository = convert.ToRepo(ctx, repo, key.Mode) + apiKey.Repository = convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: key.Mode}) } return apiKey, nil } diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index b458cd122b8a..84327de5fbe4 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -211,7 +212,7 @@ func Migrate(ctx *context.APIContext) { } log.Trace("Repository migrated: %s/%s", repoOwner.Name, form.RepoName) - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeAdmin)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeAdmin})) } func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, remoteAddr string, err error) { diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 06bfabe3d2d2..9d8497927ec4 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -258,7 +258,7 @@ func AddPushMirror(ctx *context.APIContext) { // schema: // "$ref": "#/definitions/CreatePushMirrorOption" // responses: - // "201": + // "200": // "$ref": "#/responses/PushMirror" // "403": // "$ref": "#/responses/forbidden" diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index 6fbb9e7b3a75..d2f055355de0 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -8,6 +8,7 @@ import ( "time" "code.gitea.io/gitea/models" + git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -91,12 +92,12 @@ func ApplyDiffPatch(ctx *context.APIContext) { ctx.Error(http.StatusForbidden, "Access", err) return } - if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || + if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) || models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) { ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) return } - if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) { + if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) return } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 114b93534ac3..a9f8b5b7e730 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -211,14 +211,14 @@ func Search(ctx *context.APIContext) { }) return } - accessMode, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.JSON(http.StatusInternalServerError, api.SearchError{ OK: false, Error: err.Error(), }) } - results[i] = convert.ToRepo(ctx, repo, accessMode) + results[i] = convert.ToRepo(ctx, repo, permission) } ctx.SetLinkHeader(int(count), opts.PageSize) ctx.SetTotalCountHeader(count) @@ -272,7 +272,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) } - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeOwner)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) } // Create one repository of mine @@ -419,7 +419,7 @@ func Generate(ctx *context.APIContext) { } log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeOwner)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) } // CreateOrgRepoDeprecated create one repository of the organization @@ -537,7 +537,7 @@ func Get(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } // GetByID returns a single Repository @@ -568,15 +568,15 @@ func GetByID(ctx *context.APIContext) { return } - perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return - } else if !perm.HasAccess() { + } else if !permission.HasAccess() { ctx.NotFound() return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, perm.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) } // Edit edit repository properties @@ -638,7 +638,7 @@ func Edit(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) } // updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index c1110ebce553..028e3083c60a 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -264,7 +264,7 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) { return } - combiStatus := convert.ToCombinedStatus(ctx, statuses, convert.ToRepo(ctx, repo, ctx.Repo.AccessMode)) + combiStatus := convert.ToCombinedStatus(ctx, statuses, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, combiStatus) diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index ded8edd41c06..8ff22a1193f1 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" @@ -122,12 +123,12 @@ func Transfer(ctx *context.APIContext) { if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { log.Trace("Repository transfer initiated: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) - ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeAdmin)) + ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin})) return } log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName()) - ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeAdmin)) + ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin})) } // AcceptTransfer accept a repo transfer @@ -165,7 +166,7 @@ func AcceptTransfer(ctx *context.APIContext) { return } - ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } // RejectTransfer reject a repo transfer @@ -203,7 +204,7 @@ func RejectTransfer(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode)) + ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 353d32e2142e..073d9a19f7d3 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -181,4 +181,10 @@ type swaggerParameterBodies struct { // in:body CreatePushMirrorOption api.CreatePushMirrorOption + + // in:body + UpdateUserAvatarOptions api.UpdateUserAvatarOption + + // in:body + UpdateRepoAvatarOptions api.UpdateRepoAvatarOption } diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go new file mode 100644 index 000000000000..84fa129b13ab --- /dev/null +++ b/routers/api/v1/user/avatar.go @@ -0,0 +1,63 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "encoding/base64" + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + user_service "code.gitea.io/gitea/services/user" +) + +// UpdateAvatar updates the Avatar of an User +func UpdateAvatar(ctx *context.APIContext) { + // swagger:operation POST /user/avatar user userUpdateAvatar + // --- + // summary: Update Avatar + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateUserAvatarOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + form := web.GetForm(ctx).(*api.UpdateUserAvatarOption) + + content, err := base64.StdEncoding.DecodeString(form.Image) + if err != nil { + ctx.Error(http.StatusBadRequest, "DecodeImage", err) + return + } + + err = user_service.UploadAvatar(ctx.Doer, content) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UploadAvatar", err) + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteAvatar deletes the Avatar of an User +func DeleteAvatar(ctx *context.APIContext) { + // swagger:operation DELETE /user/avatar user userDeleteAvatar + // --- + // summary: Delete Avatar + // produces: + // - application/json + // responses: + // "204": + // "$ref": "#/responses/empty" + err := user_service.DeleteAvatar(ctx.Doer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err) + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 7a8978cc4e69..86af8cb4408e 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" @@ -38,13 +39,13 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { apiRepos := make([]*api.Repository, 0, len(repos)) for i := range repos { - access, err := access_model.AccessLevel(ctx, ctx.Doer, repos[i]) + permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return } - if ctx.IsSigned && ctx.Doer.IsAdmin || access >= perm.AccessModeRead { - apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], access)) + if ctx.IsSigned && ctx.Doer.IsAdmin || permission.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead { + apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission)) } } @@ -123,11 +124,11 @@ func ListMyRepos(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "LoadOwner", err) return } - accessMode, err := access_model.AccessLevel(ctx, ctx.Doer, repo) + permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) } - results[i] = convert.ToRepo(ctx, repo, accessMode) + results[i] = convert.ToRepo(ctx, repo, permission) } ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize) diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index ad5a8bee33be..9399ad2b4db3 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -28,11 +28,11 @@ func getStarredRepos(ctx std_context.Context, user *user_model.User, private boo repos := make([]*api.Repository, len(starredRepos)) for i, starred := range starredRepos { - access, err := access_model.AccessLevel(ctx, user, starred) + permission, err := access_model.GetUserRepoPermission(ctx, starred, user) if err != nil { return nil, err } - repos[i] = convert.ToRepo(ctx, starred, access) + repos[i] = convert.ToRepo(ctx, starred, permission) } return repos, nil } diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 211f36459a83..172d9d5cc597 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -26,11 +26,11 @@ func getWatchedRepos(ctx std_context.Context, user *user_model.User, private boo repos := make([]*api.Repository, len(watchedRepos)) for i, watched := range watchedRepos { - access, err := access_model.AccessLevel(ctx, user, watched) + permission, err := access_model.GetUserRepoPermission(ctx, watched, user) if err != nil { return nil, 0, err } - repos[i] = convert.ToRepo(ctx, watched, access) + repos[i] = convert.ToRepo(ctx, watched, permission) } return repos, total, nil } diff --git a/routers/install/install.go b/routers/install/install.go index c94a30b89fcf..f121f313769d 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -99,7 +99,6 @@ func Install(ctx *context.Context) { form.DbName = setting.Database.Name form.DbPath = setting.Database.Path form.DbSchema = setting.Database.Schema - form.Charset = setting.Database.Charset curDBType := setting.Database.Type.String() var isCurDBTypeSupported bool @@ -269,7 +268,6 @@ func SubmitInstall(ctx *context.Context) { setting.Database.Name = form.DbName setting.Database.Schema = form.DbSchema setting.Database.SSLMode = form.SSLMode - setting.Database.Charset = form.Charset setting.Database.Path = form.DbPath setting.Database.LogSQL = !setting.IsProd @@ -388,7 +386,6 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd) cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema) cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode) - cfg.Section("database").Key("CHARSET").SetValue(setting.Database.Charset) cfg.Section("database").Key("PATH").SetValue(setting.Database.Path) cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 797ba8798d06..225a8c670536 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -14,12 +14,15 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/updatechecker" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/forms" + repo_service "code.gitea.io/gitea/services/repository" ) const ( @@ -133,12 +136,22 @@ func DashboardPost(ctx *context.Context) { // Run operation. if form.Op != "" { - task := cron.GetTask(form.Op) - if task != nil { - go task.RunWithUser(ctx.Doer, nil) - ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) - } else { - ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) + switch form.Op { + case "sync_repo_branches": + go func() { + if err := repo_service.AddAllRepoBranchesToSyncQueue(graceful.GetManager().ShutdownContext(), ctx.Doer.ID); err != nil { + log.Error("AddAllRepoBranchesToSyncQueue: %v: %v", ctx.Doer.ID, err) + } + }() + ctx.Flash.Success(ctx.Tr("admin.dashboard.sync_branch.started")) + default: + task := cron.GetTask(form.Op) + if task != nil { + go task.RunWithUser(ctx.Doer, nil) + ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) + } else { + ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) + } } } if form.From == "monitor" { diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index e0883a269656..bc8f6d58c927 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -383,7 +383,7 @@ func SignOut(ctx *context.Context) { }) } HandleSignOut(ctx) - ctx.Redirect(setting.AppSubURL + "/") + ctx.JSONRedirect(setting.AppSubURL + "/") } // SignUp render the register page diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 942b1f83789b..94d83818fc63 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -79,13 +79,13 @@ func Code(ctx *context.Context) { if (len(repoIDs) > 0) || isAdmin { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) if err != nil { - if code_indexer.IsAvailable() { + if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) return } ctx.Data["CodeIndexerUnavailable"] = true } else { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } loadRepoIDs := make([]int64, 0, len(searchResults)) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index be5ad1b015b7..e5f7977abd01 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -73,6 +73,14 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { orderBy = db.SearchOrderBySizeReverse case "size": orderBy = db.SearchOrderBySize + case "reversegitsize": + orderBy = db.SearchOrderByGitSizeReverse + case "gitsize": + orderBy = db.SearchOrderByGitSize + case "reverselfssize": + orderBy = db.SearchOrderByLFSSizeReverse + case "lfssize": + orderBy = db.SearchOrderByLFSSize case "moststars": orderBy = db.SearchOrderByStarsReverse case "feweststars": diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index b3f6024b6060..e525f2c43f3a 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -383,7 +383,7 @@ func ViewProject(ctx *context.Context) { ctx.HTML(http.StatusOK, tplProjectsView) } -func getActionIssues(ctx *context.Context) []*issues_model.Issue { +func getActionIssues(ctx *context.Context) issues_model.IssueList { commaSeparatedIssueIDs := ctx.FormString("issue_ids") if len(commaSeparatedIssueIDs) == 0 { return nil @@ -429,9 +429,14 @@ func UpdateIssueProject(ctx *context.Context) { return } + if err := issues.LoadProjects(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } + projectID := ctx.FormInt64("id") for _, issue := range issues { - oldProjectID := issue.ProjectID() + oldProjectID := issue.Project.ID if oldProjectID == projectID { continue } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 7c2e9d63d6d3..537bc618075c 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" actions_model "code.gitea.io/gitea/models/actions" @@ -310,6 +311,55 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro return nil } +func Logs(ctx *context_module.Context) { + runIndex := ctx.ParamsInt64("run") + jobIndex := ctx.ParamsInt64("job") + + job, _ := getRunJobs(ctx, runIndex, jobIndex) + if ctx.Written() { + return + } + if job.TaskID == 0 { + ctx.Error(http.StatusNotFound, "job is not started") + return + } + + err := job.LoadRun(ctx) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + task, err := actions_model.GetTaskByID(ctx, job.TaskID) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + if task.LogExpired { + ctx.Error(http.StatusNotFound, "logs have been cleaned up") + return + } + + reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + defer reader.Close() + + workflowName := job.Run.WorkflowID + if p := strings.Index(workflowName, "."); p > 0 { + workflowName = workflowName[0:p] + } + ctx.ServeContent(reader, &context_module.ServeHeaderOptions{ + Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID), + ContentLength: &task.LogSize, + ContentType: "text/plain", + ContentTypeCharset: "utf-8", + Disposition: "attachment", + }) +} + func Cancel(ctx *context_module.Context) { runIndex := ctx.ParamsInt64("run") diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index ea2c01856d45..f0282a71b876 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" - issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" @@ -28,32 +27,16 @@ import ( "code.gitea.io/gitea/services/forms" release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" - files_service "code.gitea.io/gitea/services/repository/files" ) const ( tplBranch base.TplName = "repo/branch/list" ) -// Branch contains the branch information -type Branch struct { - Name string - Commit *git.Commit - IsProtected bool - IsDeleted bool - IsIncluded bool - DeletedBranch *git_model.DeletedBranch - CommitsAhead int - CommitsBehind int - LatestPullRequest *issues_model.PullRequest - MergeMovedOn bool -} - // Branches render repository branch page func Branches(ctx *context.Context) { ctx.Data["Title"] = "Branches" ctx.Data["IsRepoToolbarBranches"] = true - ctx.Data["DefaultBranch"] = ctx.Repo.Repository.DefaultBranch ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls() ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror @@ -68,15 +51,15 @@ func Branches(ctx *context.Context) { } pageSize := setting.Git.BranchesRangeSize - skip := (page - 1) * pageSize - log.Debug("Branches: skip: %d limit: %d", skip, pageSize) - defaultBranchBranch, branches, branchesCount := loadBranches(ctx, skip, pageSize) - if ctx.Written() { + defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, page, pageSize) + if err != nil { + ctx.ServerError("LoadBranches", err) return } + ctx.Data["Branches"] = branches - ctx.Data["DefaultBranchBranch"] = defaultBranchBranch - pager := context.NewPagination(branchesCount, pageSize, page, 5) + ctx.Data["DefaultBranchBranch"] = defaultBranch + pager := context.NewPagination(int(branchesCount), pageSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager @@ -130,7 +113,7 @@ func RestoreBranchPost(ctx *context.Context) { if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{ Remote: ctx.Repo.Repository.RepoPath(), - Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name), + Branch: fmt.Sprintf("%s:%s%s", deletedBranch.CommitID, git.BranchPrefix, deletedBranch.Name), Env: repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository), }); err != nil { if strings.Contains(err.Error(), "already exists") { @@ -148,7 +131,7 @@ func RestoreBranchPost(ctx *context.Context) { &repo_module.PushUpdateOptions{ RefFullName: git.RefNameFromBranch(deletedBranch.Name), OldCommitID: git.EmptySHA, - NewCommitID: deletedBranch.Commit, + NewCommitID: deletedBranch.CommitID, PusherID: ctx.Doer.ID, PusherName: ctx.Doer.Name, RepoUserName: ctx.Repo.Owner.Name, @@ -166,180 +149,6 @@ func redirect(ctx *context.Context) { }) } -// loadBranches loads branches from the repository limited by page & pageSize. -// NOTE: May write to context on error. -func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, int) { - defaultBranch, err := ctx.Repo.GitRepo.GetBranch(ctx.Repo.Repository.DefaultBranch) - if err != nil { - if !git.IsErrBranchNotExist(err) { - log.Error("loadBranches: get default branch: %v", err) - ctx.ServerError("GetDefaultBranch", err) - return nil, nil, 0 - } - log.Warn("loadBranches: missing default branch %s for %-v", ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository) - } - - rawBranches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, limit) - if err != nil { - log.Error("GetBranches: %v", err) - ctx.ServerError("GetBranches", err) - return nil, nil, 0 - } - - rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) - if err != nil { - ctx.ServerError("FindRepoProtectedBranchRules", err) - return nil, nil, 0 - } - - repoIDToRepo := map[int64]*repo_model.Repository{} - repoIDToRepo[ctx.Repo.Repository.ID] = ctx.Repo.Repository - - repoIDToGitRepo := map[int64]*git.Repository{} - repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo - - var branches []*Branch - for i := range rawBranches { - if defaultBranch != nil && rawBranches[i].Name == defaultBranch.Name { - // Skip default branch - continue - } - - branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo) - if branch == nil { - return nil, nil, 0 - } - - branches = append(branches, branch) - } - - var defaultBranchBranch *Branch - if defaultBranch != nil { - // Always add the default branch - log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name) - defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo) - branches = append(branches, defaultBranchBranch) - } - - if ctx.Repo.CanWrite(unit.TypeCode) { - deletedBranches, err := getDeletedBranches(ctx) - if err != nil { - ctx.ServerError("getDeletedBranches", err) - return nil, nil, 0 - } - branches = append(branches, deletedBranches...) - } - - return defaultBranchBranch, branches, totalNumOfBranches -} - -func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches *git_model.ProtectedBranchRules, - repoIDToRepo map[int64]*repo_model.Repository, - repoIDToGitRepo map[int64]*git.Repository, -) *Branch { - log.Trace("loadOneBranch: '%s'", rawBranch.Name) - - commit, err := rawBranch.GetCommit() - if err != nil { - ctx.ServerError("GetCommit", err) - return nil - } - - branchName := rawBranch.Name - p := protectedBranches.GetFirstMatched(branchName) - isProtected := p != nil - - divergence := &git.DivergeObject{ - Ahead: -1, - Behind: -1, - } - if defaultBranch != nil { - divergence, err = files_service.CountDivergingCommits(ctx, ctx.Repo.Repository, git.BranchPrefix+branchName) - if err != nil { - log.Error("CountDivergingCommits", err) - } - } - - pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName) - if err != nil { - ctx.ServerError("GetLatestPullRequestByHeadInfo", err) - return nil - } - headCommit := commit.ID.String() - - mergeMovedOn := false - if pr != nil { - pr.HeadRepo = ctx.Repo.Repository - if err := pr.LoadIssue(ctx); err != nil { - ctx.ServerError("LoadIssue", err) - return nil - } - if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok { - pr.BaseRepo = repo - } else if err := pr.LoadBaseRepo(ctx); err != nil { - ctx.ServerError("LoadBaseRepo", err) - return nil - } else { - repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo - } - pr.Issue.Repo = pr.BaseRepo - - if pr.HasMerged { - baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] - if !ok { - baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) - if err != nil { - ctx.ServerError("OpenRepository", err) - return nil - } - defer baseGitRepo.Close() - repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo - } - pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) - if err != nil && !git.IsErrNotExist(err) { - ctx.ServerError("GetBranchCommitID", err) - return nil - } - if err == nil && headCommit != pullCommit { - // the head has moved on from the merge - we shouldn't delete - mergeMovedOn = true - } - } - } - - isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName - return &Branch{ - Name: branchName, - Commit: commit, - IsProtected: isProtected, - IsIncluded: isIncluded, - CommitsAhead: divergence.Ahead, - CommitsBehind: divergence.Behind, - LatestPullRequest: pr, - MergeMovedOn: mergeMovedOn, - } -} - -func getDeletedBranches(ctx *context.Context) ([]*Branch, error) { - branches := []*Branch{} - - deletedBranches, err := git_model.GetDeletedBranches(ctx, ctx.Repo.Repository.ID) - if err != nil { - return branches, err - } - - for i := range deletedBranches { - deletedBranches[i].LoadUser(ctx) - branches = append(branches, &Branch{ - Name: deletedBranches[i].Name, - IsDeleted: true, - DeletedBranch: deletedBranches[i], - }) - } - - return branches, nil -} - // CreateBranch creates new branch in repository func CreateBranch(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewBranchForm) @@ -380,13 +189,13 @@ func CreateBranch(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return } - if models.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { + if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.NewBranchName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return } - if models.IsErrBranchNameConflict(err) { - e := err.(models.ErrBranchNameConflict) + if git_model.IsErrBranchNameConflict(err) { + e := err.(git_model.ErrBranchNameConflict) ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) return diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go index 48bc6959e075..5017d0225227 100644 --- a/routers/web/repo/cherry_pick.go +++ b/routers/web/repo/cherry_pick.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/models" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -124,9 +125,9 @@ func CherryPickPost(ctx *context.Context) { // First lets try the simple plain read-tree -m approach opts.Content = sha if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { - if models.IsErrBranchAlreadyExists(err) { + if git_model.IsErrBranchAlreadyExists(err) { // User has specified a branch that already exists - branchErr := err.(models.ErrBranchAlreadyExists) + branchErr := err.(git_model.ErrBranchAlreadyExists) ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) return @@ -161,9 +162,9 @@ func CherryPickPost(ctx *context.Context) { ctx.Data["FileContent"] = opts.Content if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if models.IsErrBranchAlreadyExists(err) { + if git_model.IsErrBranchAlreadyExists(err) { // User has specified a branch that already exists - branchErr := err.(models.ErrBranchAlreadyExists) + branchErr := err.(git_model.ErrBranchAlreadyExists) ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) return diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 0ca1f90547ef..7089c219ad25 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -16,6 +16,7 @@ import ( "path/filepath" "strings" + "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" @@ -683,7 +684,13 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor } defer gitRepo.Close() - branches, _, err = gitRepo.GetBranchNames(0, 0) + branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ + RepoID: repo.ID, + ListOptions: db.ListOptions{ + ListAll: true, + }, + IsDeletedBranch: util.OptionalBoolFalse, + }) if err != nil { return nil, nil, err } @@ -734,7 +741,13 @@ func CompareDiff(ctx *context.Context) { return } - headBranches, _, err := ci.HeadGitRepo.GetBranchNames(0, 0) + headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ + RepoID: ci.HeadRepo.ID, + ListOptions: db.ListOptions{ + ListAll: true, + }, + IsDeletedBranch: util.OptionalBoolFalse, + }) if err != nil { ctx.ServerError("GetBranches", err) return diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 2fea8a9532b9..a63b08126c93 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -327,10 +327,10 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b } else { ctx.Error(http.StatusInternalServerError, err.Error()) } - } else if models.IsErrBranchAlreadyExists(err) { + } else if git_model.IsErrBranchAlreadyExists(err) { // For when a user specifies a new branch that already exists ctx.Data["Err_NewBranchName"] = true - if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok { + if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) } else { ctx.Error(http.StatusInternalServerError, err.Error()) @@ -529,9 +529,9 @@ func DeleteFilePost(ctx *context.Context) { } else { ctx.Error(http.StatusInternalServerError, err.Error()) } - } else if models.IsErrBranchAlreadyExists(err) { + } else if git_model.IsErrBranchAlreadyExists(err) { // For when a user specifies a new branch that already exists - if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok { + if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form) } else { ctx.Error(http.StatusInternalServerError, err.Error()) @@ -731,10 +731,10 @@ func UploadFilePost(ctx *context.Context) { } else if git.IsErrBranchNotExist(err) { branchErr := err.(git.ErrBranchNotExist) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form) - } else if models.IsErrBranchAlreadyExists(err) { + } else if git_model.IsErrBranchAlreadyExists(err) { // For when a user specifies a new branch that already exists ctx.Data["Err_NewBranchName"] = true - branchErr := err.(models.ErrBranchAlreadyExists) + branchErr := err.(git_model.ErrBranchAlreadyExists) ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form) } else if git.IsErrPushOutOfDate(err) { ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a9ce1cc1e752..4f14cc381f9d 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -191,7 +191,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti if len(keyword) > 0 { issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{repo.ID}, keyword) if err != nil { - if issue_indexer.IsAvailable() { + if issue_indexer.IsAvailable(ctx) { ctx.ServerError("issueIndexer.Search", err) return } @@ -785,7 +785,13 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull return nil } - brs, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0) + brs, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ + RepoID: ctx.Repo.Repository.ID, + ListOptions: db.ListOptions{ + ListAll: true, + }, + IsDeletedBranch: util.OptionalBoolFalse, + }) if err != nil { ctx.ServerError("GetBranches", err) return nil @@ -1647,9 +1653,22 @@ func ViewIssue(ctx *context.Context) { return } } else if comment.Type == issues_model.CommentTypeAddTimeManual || - comment.Type == issues_model.CommentTypeStopTracking { + comment.Type == issues_model.CommentTypeStopTracking || + comment.Type == issues_model.CommentTypeDeleteTimeManual { // drop error since times could be pruned from DB.. _ = comment.LoadTime() + if comment.Content != "" { + // Content before v1.21 did store the formated string instead of seconds, + // so "|" is used as delimeter to mark the new format + if comment.Content[0] != '|' { + // handle old time comments that have formatted text stored + comment.RenderedContent = comment.Content + comment.Content = "" + } else { + // else it's just a duration in seconds to pass on to the frontend + comment.Content = comment.Content[1:] + } + } } if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull { @@ -1971,7 +1990,7 @@ func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) { } } -func getActionIssues(ctx *context.Context) []*issues_model.Issue { +func getActionIssues(ctx *context.Context) issues_model.IssueList { commaSeparatedIssueIDs := ctx.FormString("issue_ids") if len(commaSeparatedIssueIDs) == 0 { return nil @@ -2736,7 +2755,7 @@ func UpdateIssueStatus(ctx *context.Context) { log.Warn("Unrecognized action: %s", action) } - if _, err := issues_model.IssueList(issues).LoadRepositories(ctx); err != nil { + if _, err := issues.LoadRepositories(ctx); err != nil { ctx.ServerError("LoadRepositories", err) return } diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go index 08b76e555f11..93f5a588d912 100644 --- a/routers/web/repo/issue_lock.go +++ b/routers/web/repo/issue_lock.go @@ -20,14 +20,12 @@ func LockIssue(ctx *context.Context) { } if issue.IsLocked { - ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate")) - ctx.Redirect(issue.Link()) + ctx.JSONError(ctx.Tr("repo.issues.lock_duplicate")) return } if !form.HasValidReason() { - ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason")) - ctx.Redirect(issue.Link()) + ctx.JSONError(ctx.Tr("repo.issues.lock.unknown_reason")) return } @@ -40,7 +38,7 @@ func LockIssue(ctx *context.Context) { return } - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } // UnlockIssue unlocks a previously locked issue. @@ -51,8 +49,7 @@ func UnlockIssue(ctx *context.Context) { } if !issue.IsLocked { - ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error")) - ctx.Redirect(issue.Link()) + ctx.JSONError(ctx.Tr("repo.issues.unlock_error")) return } @@ -64,5 +61,5 @@ func UnlockIssue(ctx *context.Context) { return } - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go index 6586372fc531..7c1a306e6cef 100644 --- a/routers/web/repo/issue_pin.go +++ b/routers/web/repo/issue_pin.go @@ -31,7 +31,7 @@ func IssuePinOrUnpin(ctx *context.Context) { return } - ctx.Redirect(issue.Link()) + ctx.JSONRedirect(issue.Link()) } // IssueUnpin unpins a Issue diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index 5c38b31154fe..216550ca996c 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -5,6 +5,7 @@ package repo import ( "fmt" + "strconv" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" @@ -88,3 +89,27 @@ func SetWhitespaceBehavior(ctx *context.Context) { ctx.Data["WhitespaceBehavior"] = whitespaceBehavior } } + +// SetShowOutdatedComments set the show outdated comments option as context variable +func SetShowOutdatedComments(ctx *context.Context) { + showOutdatedCommentsValue := ctx.FormString("show-outdated") + // var showOutdatedCommentsValue string + + if showOutdatedCommentsValue != "true" && showOutdatedCommentsValue != "false" { + // invalid or no value for this form string -> use default or stored user setting + if ctx.IsSigned { + showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, "false") + } else { + // not logged in user -> use the default value + showOutdatedCommentsValue = "false" + } + } else { + // valid value -> update user setting if user is logged in + if ctx.IsSigned { + _ = user_model.SetUserSetting(ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue) + } + } + + showOutdatedComments, _ := strconv.ParseBool(showOutdatedCommentsValue) + ctx.Data["ShowOutdatedComments"] = showOutdatedComments +} diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index efb4662496c3..5faf9f4fa9bc 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -7,6 +7,7 @@ import ( "strings" "code.gitea.io/gitea/models" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -94,9 +95,9 @@ func NewDiffPatchPost(ctx *context.Context) { Content: strings.ReplaceAll(form.Content, "\r", ""), }) if err != nil { - if models.IsErrBranchAlreadyExists(err) { + if git_model.IsErrBranchAlreadyExists(err) { // User has specified a branch that already exists - branchErr := err.(models.ErrBranchAlreadyExists) + branchErr := err.(git_model.ErrBranchAlreadyExists) ctx.Data["Err_NewBranchName"] = true ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) return diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 5ee5ead12177..6da9edfd0b80 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -378,9 +378,14 @@ func UpdateIssueProject(ctx *context.Context) { return } + if err := issues.LoadProjects(ctx); err != nil { + ctx.ServerError("LoadProjects", err) + return + } + projectID := ctx.FormInt64("id") for _, issue := range issues { - oldProjectID := issue.ProjectID() + oldProjectID := issue.Project.ID if oldProjectID == projectID { continue } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index ef9d5856da0e..950979a6ed44 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -761,7 +761,7 @@ func ViewPullFiles(ctx *context.Context) { "numberOfViewedFiles": diff.NumViewedFiles, } - if err = diff.LoadComments(ctx, issue, ctx.Doer); err != nil { + if err = diff.LoadComments(ctx, issue, ctx.Doer, ctx.Data["ShowOutdatedComments"].(bool)); err != nil { ctx.ServerError("LoadComments", err) return } @@ -1493,7 +1493,7 @@ func UpdatePullRequestTarget(ctx *context.Context) { "error": err.Error(), "user_error": errorMessage, }) - } else if models.IsErrBranchesEqual(err) { + } else if git_model.IsErrBranchesEqual(err) { errorMessage := ctx.Tr("repo.pulls.nothing_to_compare") ctx.Flash.Error(errorMessage) diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go index 69d36ff4a47e..5aa581136705 100644 --- a/routers/web/repo/pull_review.go +++ b/routers/web/repo/pull_review.go @@ -159,7 +159,7 @@ func UpdateResolveConversation(ctx *context.Context) { } func renderConversation(ctx *context.Context, comment *issues_model.Comment) { - comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line) + comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool)) if err != nil { ctx.ServerError("FetchCodeCommentsByLine", err) return diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index a043198472ae..3c0fa4bc00ea 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -45,13 +45,13 @@ func Search(ctx *context.Context) { total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID}, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) if err != nil { - if code_indexer.IsAvailable() { + if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) return } ctx.Data["CodeIndexerUnavailable"] = true } else { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } ctx.Data["SourcePath"] = ctx.Repo.Repository.Link() diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go new file mode 100644 index 000000000000..ec673ca28868 --- /dev/null +++ b/routers/web/repo/setting/avatar.go @@ -0,0 +1,76 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "fmt" + "io" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/typesniffer" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" + repo_service "code.gitea.io/gitea/services/repository" +) + +// UpdateAvatarSetting update repo's avatar +func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error { + ctxRepo := ctx.Repo.Repository + + if form.Avatar == nil { + // No avatar is uploaded and we not removing it here. + // No random avatar generated here. + // Just exit, no action. + if ctxRepo.CustomAvatarRelativePath() == "" { + log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID) + } + return nil + } + + r, err := form.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %w", err) + } + defer r.Close() + + if form.Avatar.Size > setting.Avatar.MaxFileSize { + return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) + } + + data, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("io.ReadAll: %w", err) + } + st := typesniffer.DetectContentType(data) + if !(st.IsImage() && !st.IsSvgImage()) { + return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil { + return fmt.Errorf("UploadAvatar: %w", err) + } + return nil +} + +// SettingsAvatar save new POSTed repository avatar +func SettingsAvatar(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.AvatarForm) + form.Source = forms.AvatarLocal + if err := UpdateAvatarSetting(ctx, *form); err != nil { + ctx.Flash.Error(err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success")) + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} + +// SettingsDeleteAvatar delete repository avatar +func SettingsDeleteAvatar(ctx *context.Context) { + if err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository); err != nil { + ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go new file mode 100644 index 000000000000..d3a9959104a4 --- /dev/null +++ b/routers/web/repo/setting/collaboration.go @@ -0,0 +1,210 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/utils" + "code.gitea.io/gitea/services/mailer" + org_service "code.gitea.io/gitea/services/org" +) + +// Collaboration render a repository's collaboration page +func Collaboration(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration") + ctx.Data["PageIsSettingsCollaboration"] = true + + users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) + if err != nil { + ctx.ServerError("GetCollaborators", err) + return + } + ctx.Data["Collaborators"] = users + + teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository) + if err != nil { + ctx.ServerError("GetRepoTeams", err) + return + } + ctx.Data["Teams"] = teams + ctx.Data["Repo"] = ctx.Repo.Repository + ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID + ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName + ctx.Data["Org"] = ctx.Repo.Repository.Owner + ctx.Data["Units"] = unit_model.Units + + ctx.HTML(http.StatusOK, tplCollaboration) +} + +// CollaborationPost response for actions for a collaboration of a repository +func CollaborationPost(ctx *context.Context) { + name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("collaborator"))) + if len(name) == 0 || ctx.Repo.Owner.LowerName == name { + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + return + } + + u, err := user_model.GetUserByName(ctx, name) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + } else { + ctx.ServerError("GetUserByName", err) + } + return + } + + if !u.IsActive { + ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + return + } + + // Organization is not allowed to be added as a collaborator. + if u.IsOrganization() { + ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + return + } + + if got, err := repo_model.IsCollaborator(ctx, ctx.Repo.Repository.ID, u.ID); err == nil && got { + ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + // find the owner team of the organization the repo belongs too and + // check if the user we're trying to add is an owner. + if ctx.Repo.Repository.Owner.IsOrganization() { + if isOwner, err := organization.IsOrganizationOwner(ctx, ctx.Repo.Repository.Owner.ID, u.ID); err != nil { + ctx.ServerError("IsOrganizationOwner", err) + return + } else if isOwner { + ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_owner")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + return + } + } + + if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil { + ctx.ServerError("AddCollaborator", err) + return + } + + if setting.Service.EnableNotifyMail { + mailer.SendCollaboratorMail(u, ctx.Doer, ctx.Repo.Repository) + } + + ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) +} + +// ChangeCollaborationAccessMode response for changing access of a collaboration +func ChangeCollaborationAccessMode(ctx *context.Context) { + if err := repo_model.ChangeCollaborationAccessMode( + ctx, + ctx.Repo.Repository, + ctx.FormInt64("uid"), + perm.AccessMode(ctx.FormInt("mode"))); err != nil { + log.Error("ChangeCollaborationAccessMode: %v", err) + } +} + +// DeleteCollaboration delete a collaboration for a repository +func DeleteCollaboration(ctx *context.Context) { + if err := models.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { + ctx.Flash.Error("DeleteCollaboration: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": ctx.Repo.RepoLink + "/settings/collaboration", + }) +} + +// AddTeamPost response for adding a team to a repository +func AddTeamPost(ctx *context.Context) { + if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { + ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("team"))) + if len(name) == 0 { + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, name) + if err != nil { + if organization.IsErrTeamNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.team_not_exist")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + } else { + ctx.ServerError("GetTeam", err) + } + return + } + + if team.OrgID != ctx.Repo.Repository.OwnerID { + ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + if organization.HasTeamRepo(ctx, ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) { + ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + if err = org_service.TeamAddRepository(team, ctx.Repo.Repository); err != nil { + ctx.ServerError("TeamAddRepository", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") +} + +// DeleteTeam response for deleting a team from a repository +func DeleteTeam(ctx *context.Context) { + if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { + ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") + return + } + + team, err := organization.GetTeamByID(ctx, ctx.FormInt64("id")) + if err != nil { + ctx.ServerError("GetTeamByID", err) + return + } + + if err = models.RemoveRepository(team, ctx.Repo.Repository.ID); err != nil { + ctx.ServerError("team.RemoveRepositorys", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success")) + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": ctx.Repo.RepoLink + "/settings/collaboration", + }) +} diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go new file mode 100644 index 000000000000..9776095672eb --- /dev/null +++ b/routers/web/repo/setting/deploy_key.go @@ -0,0 +1,111 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + asymkey_service "code.gitea.io/gitea/services/asymkey" + "code.gitea.io/gitea/services/forms" +) + +// 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.Tr("secrets.secrets") + ctx.Data["PageIsSettingsKeys"] = true + ctx.Data["DisableSSH"] = setting.SSH.Disabled + + keys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID}) + if err != nil { + ctx.ServerError("ListDeployKeys", err) + return + } + ctx.Data["Deploykeys"] = keys + + ctx.HTML(http.StatusOK, tplDeployKeys) +} + +// DeployKeysPost response for adding a deploy key of a repository +func DeployKeysPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.AddKeyForm) + ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + ctx.Data["PageIsSettingsKeys"] = true + ctx.Data["DisableSSH"] = setting.SSH.Disabled + + keys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID}) + if err != nil { + ctx.ServerError("ListDeployKeys", err) + return + } + ctx.Data["Deploykeys"] = keys + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplDeployKeys) + return + } + + content, err := asymkey_model.CheckPublicKeyString(form.Content) + if err != nil { + if db.IsErrSSHDisabled(err) { + ctx.Flash.Info(ctx.Tr("settings.ssh_disabled")) + } else if asymkey_model.IsErrKeyUnableVerify(err) { + ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) + } else if err == asymkey_model.ErrKeyIsPrivate { + ctx.Data["HasError"] = true + ctx.Data["Err_Content"] = true + ctx.Flash.Error(ctx.Tr("form.must_use_public_key")) + } else { + ctx.Data["HasError"] = true + ctx.Data["Err_Content"] = true + ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") + return + } + + key, err := asymkey_model.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable) + if err != nil { + ctx.Data["HasError"] = true + switch { + case asymkey_model.IsErrDeployKeyAlreadyExist(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form) + case asymkey_model.IsErrKeyAlreadyExist(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form) + case asymkey_model.IsErrKeyNameAlreadyUsed(err): + ctx.Data["Err_Title"] = true + ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) + case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err): + ctx.Data["Err_Title"] = true + ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) + default: + ctx.ServerError("AddDeployKey", err) + } + return + } + + log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID) + ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name)) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") +} + +// DeleteDeployKey response for deleting a deploy key +func DeleteDeployKey(ctx *context.Context) { + if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil { + ctx.Flash.Error("DeleteDeployKey: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success")) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": ctx.Repo.RepoLink + "/settings/keys", + }) +} diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go new file mode 100644 index 000000000000..551327d44b47 --- /dev/null +++ b/routers/web/repo/setting/git_hooks.go @@ -0,0 +1,65 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" +) + +// GitHooks hooks of a repository +func GitHooks(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") + ctx.Data["PageIsSettingsGitHooks"] = true + + hooks, err := ctx.Repo.GitRepo.Hooks() + if err != nil { + ctx.ServerError("Hooks", err) + return + } + ctx.Data["Hooks"] = hooks + + ctx.HTML(http.StatusOK, tplGithooks) +} + +// GitHooksEdit render for editing a hook of repository page +func GitHooksEdit(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") + ctx.Data["PageIsSettingsGitHooks"] = true + + name := ctx.Params(":name") + hook, err := ctx.Repo.GitRepo.GetHook(name) + if err != nil { + if err == git.ErrNotValidHook { + ctx.NotFound("GetHook", err) + } else { + ctx.ServerError("GetHook", err) + } + return + } + ctx.Data["Hook"] = hook + ctx.HTML(http.StatusOK, tplGithookEdit) +} + +// GitHooksEditPost response for editing a git hook of a repository +func GitHooksEditPost(ctx *context.Context) { + name := ctx.Params(":name") + hook, err := ctx.Repo.GitRepo.GetHook(name) + if err != nil { + if err == git.ErrNotValidHook { + ctx.NotFound("GetHook", err) + } else { + ctx.ServerError("GetHook", err) + } + return + } + hook.Content = ctx.FormString("content") + if err = hook.Update(); err != nil { + ctx.ServerError("hook.Update", err) + return + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git") +} diff --git a/routers/web/repo/lfs.go b/routers/web/repo/setting/lfs.go similarity index 99% rename from routers/web/repo/lfs.go rename to routers/web/repo/setting/lfs.go index 9957869c999a..d478acdde0db 100644 --- a/routers/web/repo/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo +package setting import ( "bytes" diff --git a/routers/web/repo/setting/main_test.go b/routers/web/repo/setting/main_test.go new file mode 100644 index 000000000000..5a6fa5621775 --- /dev/null +++ b/routers/web/repo/setting/main_test.go @@ -0,0 +1,17 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + GiteaRootPath: filepath.Join("..", "..", "..", ".."), + }) +} diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting/protected_branch.go similarity index 99% rename from routers/web/repo/setting_protected_branch.go rename to routers/web/repo/setting/protected_branch.go index 1a944799c23f..962b85d0cd95 100644 --- a/routers/web/repo/setting_protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -1,7 +1,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo +package setting import ( "fmt" @@ -286,7 +286,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { } // FIXME: since we only need to recheck files protected rules, we could improve this - matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName) + matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName) if err != nil { ctx.ServerError("FindAllMatchedBranches", err) return diff --git a/routers/web/repo/tag.go b/routers/web/repo/setting/protected_tag.go similarity index 97% rename from routers/web/repo/tag.go rename to routers/web/repo/setting/protected_tag.go index 95bc6dfce7f8..aafbd19e80ed 100644 --- a/routers/web/repo/tag.go +++ b/routers/web/repo/setting/protected_tag.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo +package setting import ( "fmt" @@ -19,8 +19,12 @@ import ( "code.gitea.io/gitea/services/forms" ) +const ( + tplTags base.TplName = "repo/settings/tags" +) + // Tags render the page to protect tags -func Tags(ctx *context.Context) { +func ProtectedTags(ctx *context.Context) { if setTagsContext(ctx) != nil { return } diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting/setting.go similarity index 70% rename from routers/web/repo/setting.go rename to routers/web/repo/setting/setting.go index 4481ff4f04f1..b33660ffc9cc 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting/setting.go @@ -2,22 +2,18 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo +package setting import ( - "errors" "fmt" - "io" "net/http" "strconv" "strings" "time" "code.gitea.io/gitea/models" - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -32,17 +28,13 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/utils" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/mailer" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" - org_service "code.gitea.io/gitea/services/org" repo_service "code.gitea.io/gitea/services/repository" wiki_service "code.gitea.io/gitea/services/wiki" ) @@ -51,7 +43,6 @@ const ( tplSettingsOptions base.TplName = "repo/settings/options" tplCollaboration base.TplName = "repo/settings/collaboration" tplBranches base.TplName = "repo/settings/branches" - tplTags base.TplName = "repo/settings/tags" tplGithooks base.TplName = "repo/settings/githooks" tplGithookEdit base.TplName = "repo/settings/githook_edit" tplDeployKeys base.TplName = "repo/settings/deploy_keys" @@ -895,398 +886,6 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form) } -// Collaboration render a repository's collaboration page -func Collaboration(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration") - ctx.Data["PageIsSettingsCollaboration"] = true - - users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) - if err != nil { - ctx.ServerError("GetCollaborators", err) - return - } - ctx.Data["Collaborators"] = users - - teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository) - if err != nil { - ctx.ServerError("GetRepoTeams", err) - return - } - ctx.Data["Teams"] = teams - ctx.Data["Repo"] = ctx.Repo.Repository - ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID - ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName - ctx.Data["Org"] = ctx.Repo.Repository.Owner - ctx.Data["Units"] = unit_model.Units - - ctx.HTML(http.StatusOK, tplCollaboration) -} - -// CollaborationPost response for actions for a collaboration of a repository -func CollaborationPost(ctx *context.Context) { - name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("collaborator"))) - if len(name) == 0 || ctx.Repo.Owner.LowerName == name { - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) - return - } - - u, err := user_model.GetUserByName(ctx, name) - if err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.Flash.Error(ctx.Tr("form.user_not_exist")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) - } else { - ctx.ServerError("GetUserByName", err) - } - return - } - - if !u.IsActive { - ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_inactive_user")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) - return - } - - // Organization is not allowed to be added as a collaborator. - if u.IsOrganization() { - ctx.Flash.Error(ctx.Tr("repo.settings.org_not_allowed_to_be_collaborator")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) - return - } - - if got, err := repo_model.IsCollaborator(ctx, ctx.Repo.Repository.ID, u.ID); err == nil && got { - ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - return - } - - // find the owner team of the organization the repo belongs too and - // check if the user we're trying to add is an owner. - if ctx.Repo.Repository.Owner.IsOrganization() { - if isOwner, err := organization.IsOrganizationOwner(ctx, ctx.Repo.Repository.Owner.ID, u.ID); err != nil { - ctx.ServerError("IsOrganizationOwner", err) - return - } else if isOwner { - ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_owner")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) - return - } - } - - if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil { - ctx.ServerError("AddCollaborator", err) - return - } - - if setting.Service.EnableNotifyMail { - mailer.SendCollaboratorMail(u, ctx.Doer, ctx.Repo.Repository) - } - - ctx.Flash.Success(ctx.Tr("repo.settings.add_collaborator_success")) - ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) -} - -// ChangeCollaborationAccessMode response for changing access of a collaboration -func ChangeCollaborationAccessMode(ctx *context.Context) { - if err := repo_model.ChangeCollaborationAccessMode( - ctx, - ctx.Repo.Repository, - ctx.FormInt64("uid"), - perm.AccessMode(ctx.FormInt("mode"))); err != nil { - log.Error("ChangeCollaborationAccessMode: %v", err) - } -} - -// DeleteCollaboration delete a collaboration for a repository -func DeleteCollaboration(ctx *context.Context) { - if err := models.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { - ctx.Flash.Error("DeleteCollaboration: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Repo.RepoLink + "/settings/collaboration", - }) -} - -// AddTeamPost response for adding a team to a repository -func AddTeamPost(ctx *context.Context) { - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { - ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - return - } - - name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("team"))) - if len(name) == 0 { - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - return - } - - team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, name) - if err != nil { - if organization.IsErrTeamNotExist(err) { - ctx.Flash.Error(ctx.Tr("form.team_not_exist")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - } else { - ctx.ServerError("GetTeam", err) - } - return - } - - if team.OrgID != ctx.Repo.Repository.OwnerID { - ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - return - } - - if organization.HasTeamRepo(ctx, ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) { - ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - return - } - - if err = org_service.TeamAddRepository(team, ctx.Repo.Repository); err != nil { - ctx.ServerError("TeamAddRepository", err) - return - } - - ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") -} - -// DeleteTeam response for deleting a team from a repository -func DeleteTeam(ctx *context.Context) { - if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { - ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") - return - } - - team, err := organization.GetTeamByID(ctx, ctx.FormInt64("id")) - if err != nil { - ctx.ServerError("GetTeamByID", err) - return - } - - if err = models.RemoveRepository(team, ctx.Repo.Repository.ID); err != nil { - ctx.ServerError("team.RemoveRepositorys", err) - return - } - - ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success")) - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Repo.RepoLink + "/settings/collaboration", - }) -} - -// GitHooks hooks of a repository -func GitHooks(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") - ctx.Data["PageIsSettingsGitHooks"] = true - - hooks, err := ctx.Repo.GitRepo.Hooks() - if err != nil { - ctx.ServerError("Hooks", err) - return - } - ctx.Data["Hooks"] = hooks - - ctx.HTML(http.StatusOK, tplGithooks) -} - -// GitHooksEdit render for editing a hook of repository page -func GitHooksEdit(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.settings.githooks") - ctx.Data["PageIsSettingsGitHooks"] = true - - name := ctx.Params(":name") - hook, err := ctx.Repo.GitRepo.GetHook(name) - if err != nil { - if err == git.ErrNotValidHook { - ctx.NotFound("GetHook", err) - } else { - ctx.ServerError("GetHook", err) - } - return - } - ctx.Data["Hook"] = hook - ctx.HTML(http.StatusOK, tplGithookEdit) -} - -// GitHooksEditPost response for editing a git hook of a repository -func GitHooksEditPost(ctx *context.Context) { - name := ctx.Params(":name") - hook, err := ctx.Repo.GitRepo.GetHook(name) - if err != nil { - if err == git.ErrNotValidHook { - ctx.NotFound("GetHook", err) - } else { - ctx.ServerError("GetHook", err) - } - return - } - hook.Content = ctx.FormString("content") - if err = hook.Update(); err != nil { - ctx.ServerError("hook.Update", err) - return - } - ctx.Redirect(ctx.Repo.RepoLink + "/settings/hooks/git") -} - -// 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.Tr("secrets.secrets") - ctx.Data["PageIsSettingsKeys"] = true - ctx.Data["DisableSSH"] = setting.SSH.Disabled - - keys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID}) - if err != nil { - ctx.ServerError("ListDeployKeys", err) - return - } - ctx.Data["Deploykeys"] = keys - - ctx.HTML(http.StatusOK, tplDeployKeys) -} - -// DeployKeysPost response for adding a deploy key of a repository -func DeployKeysPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.AddKeyForm) - ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") - ctx.Data["PageIsSettingsKeys"] = true - ctx.Data["DisableSSH"] = setting.SSH.Disabled - - keys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID}) - if err != nil { - ctx.ServerError("ListDeployKeys", err) - return - } - ctx.Data["Deploykeys"] = keys - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplDeployKeys) - return - } - - content, err := asymkey_model.CheckPublicKeyString(form.Content) - if err != nil { - if db.IsErrSSHDisabled(err) { - ctx.Flash.Info(ctx.Tr("settings.ssh_disabled")) - } else if asymkey_model.IsErrKeyUnableVerify(err) { - ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) - } else if err == asymkey_model.ErrKeyIsPrivate { - ctx.Data["HasError"] = true - ctx.Data["Err_Content"] = true - ctx.Flash.Error(ctx.Tr("form.must_use_public_key")) - } else { - ctx.Data["HasError"] = true - ctx.Data["Err_Content"] = true - ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) - } - ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") - return - } - - key, err := asymkey_model.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable) - if err != nil { - ctx.Data["HasError"] = true - switch { - case asymkey_model.IsErrDeployKeyAlreadyExist(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form) - case asymkey_model.IsErrKeyAlreadyExist(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form) - case asymkey_model.IsErrKeyNameAlreadyUsed(err): - ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) - case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err): - ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) - default: - ctx.ServerError("AddDeployKey", err) - } - return - } - - log.Trace("Deploy key added: %d", ctx.Repo.Repository.ID) - ctx.Flash.Success(ctx.Tr("repo.settings.add_key_success", key.Name)) - ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") -} - -// DeleteDeployKey response for deleting a deploy key -func DeleteDeployKey(ctx *context.Context) { - if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil { - ctx.Flash.Error("DeleteDeployKey: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success")) - } - - ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Repo.RepoLink + "/settings/keys", - }) -} - -// UpdateAvatarSetting update repo's avatar -func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error { - ctxRepo := ctx.Repo.Repository - - if form.Avatar == nil { - // No avatar is uploaded and we not removing it here. - // No random avatar generated here. - // Just exit, no action. - if ctxRepo.CustomAvatarRelativePath() == "" { - log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID) - } - return nil - } - - r, err := form.Avatar.Open() - if err != nil { - return fmt.Errorf("Avatar.Open: %w", err) - } - defer r.Close() - - if form.Avatar.Size > setting.Avatar.MaxFileSize { - return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) - } - - data, err := io.ReadAll(r) - if err != nil { - return fmt.Errorf("io.ReadAll: %w", err) - } - st := typesniffer.DetectContentType(data) - if !(st.IsImage() && !st.IsSvgImage()) { - return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) - } - if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil { - return fmt.Errorf("UploadAvatar: %w", err) - } - return nil -} - -// SettingsAvatar save new POSTed repository avatar -func SettingsAvatar(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.AvatarForm) - form.Source = forms.AvatarLocal - if err := UpdateAvatarSetting(ctx, *form); err != nil { - ctx.Flash.Error(err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success")) - } - ctx.Redirect(ctx.Repo.RepoLink + "/settings") -} - -// SettingsDeleteAvatar delete repository avatar -func SettingsDeleteAvatar(ctx *context.Context) { - if err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository); err != nil { - ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) - } - ctx.Redirect(ctx.Repo.RepoLink + "/settings") -} - func selectPushMirrorByForm(ctx *context.Context, form *forms.RepoSettingForm, repo *repo_model.Repository) (*repo_model.PushMirror, error) { id, err := strconv.ParseInt(form.PushMirrorID, 10, 64) if err != nil { diff --git a/routers/web/repo/settings_test.go b/routers/web/repo/setting/settings_test.go similarity index 99% rename from routers/web/repo/settings_test.go rename to routers/web/repo/setting/settings_test.go index a33e92c82113..6f7c844ce7a0 100644 --- a/routers/web/repo/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -1,7 +1,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo +package setting import ( "net/http" diff --git a/routers/web/repo/webhook.go b/routers/web/repo/setting/webhook.go similarity index 99% rename from routers/web/repo/webhook.go rename to routers/web/repo/setting/webhook.go index 5139c0b09161..a1cedd9a31a9 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -2,7 +2,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repo +package setting import ( "errors" @@ -13,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/base" @@ -685,7 +686,7 @@ func TestWebhook(ctx *context.Context) { Commits: []*api.PayloadCommit{apiCommit}, TotalCommits: 1, HeadCommit: apiCommit, - Repo: convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeNone), + Repo: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}), Pusher: apiUser, Sender: apiUser, } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 115418887d30..22cfe6dd2587 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -273,6 +273,16 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { return nil, nil } + if rctx.SidebarTocNode != nil { + sb := &strings.Builder{} + err = markdown.SpecializedMarkdown().Renderer().Render(sb, nil, rctx.SidebarTocNode) + if err != nil { + log.Error("Failed to render wiki sidebar TOC: %v", err) + } else { + ctx.Data["sidebarTocContent"] = sb.String() + } + } + if !isSideBar { buf.Reset() ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent) @@ -303,16 +313,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Data["footerPresent"] = false } - if rctx.SidebarTocNode != nil { - sb := &strings.Builder{} - err = markdown.SpecializedMarkdown().Renderer().Render(sb, nil, rctx.SidebarTocNode) - if err != nil { - log.Error("Failed to render wiki sidebar TOC: %v", err) - } else { - ctx.Data["sidebarTocContent"] = sb.String() - } - } - // get commit count - wiki revisions commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount diff --git a/routers/web/user/code.go b/routers/web/user/code.go index b3adbcb8d3a8..15524de7d651 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -71,13 +71,13 @@ func CodeSearch(ctx *context.Context) { if len(repoIDs) > 0 { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch) if err != nil { - if code_indexer.IsAvailable() { + if code_indexer.IsAvailable(ctx) { ctx.ServerError("SearchResults", err) return } ctx.Data["CodeIndexerUnavailable"] = true } else { - ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable() + ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } loadRepoIDs := make([]int64, 0, len(searchResults)) diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 81a26da82728..20141914b655 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -420,7 +420,13 @@ func PackageSettingsPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("packages.settings.delete.success")) } - ctx.Redirect(ctx.Package.Owner.HomeLink() + "/-/packages") + redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages" + // redirect to the package if there are still versions available + if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID}); has { + redirectURL = ctx.Package.Descriptor.PackageWebLink() + } + + ctx.Redirect(redirectURL) return } } diff --git a/routers/web/web.go b/routers/web/web.go index a7573b38f559..5dd7be120d01 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -279,32 +279,32 @@ func registerRoutes(m *web.Route) { } addWebhookAddRoutes := func() { - m.Get("/{type}/new", repo.WebhooksNew) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + m.Get("/{type}/new", repo_setting.WebhooksNew) + m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksNewPost) + m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksNewPost) + m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksNewPost) + m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksNewPost) + m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksNewPost) + m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksNewPost) + m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksNewPost) + m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksNewPost) + m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksNewPost) + m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksNewPost) + m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksNewPost) } addWebhookEditRoutes := func() { - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo_setting.GiteaHooksEditPost) + m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo_setting.GogsHooksEditPost) + m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo_setting.SlackHooksEditPost) + m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo_setting.DiscordHooksEditPost) + m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo_setting.DingtalkHooksEditPost) + m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo_setting.TelegramHooksEditPost) + m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo_setting.MatrixHooksEditPost) + m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo_setting.MSTeamsHooksEditPost) + m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo_setting.FeishuHooksEditPost) + m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo_setting.WechatworkHooksEditPost) + m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo_setting.PackagistHooksEditPost) } addSettingVariablesRoutes := func() { @@ -515,8 +515,8 @@ func registerRoutes(m *web.Route) { m.Post("/delete", user_setting.DeleteWebhook) addWebhookAddRoutes() m.Group("/{id}", func() { - m.Get("", repo.WebHooksEdit) - m.Post("/replay/{uuid}", repo.ReplayWebhook) + m.Get("", repo_setting.WebHooksEdit) + m.Post("/replay/{uuid}", repo_setting.ReplayWebhook) }) addWebhookEditRoutes() }, webhooksEnabled) @@ -604,8 +604,8 @@ func registerRoutes(m *web.Route) { m.Get("", admin.DefaultOrSystemWebhooks) m.Post("/delete", admin.DeleteDefaultOrSystemWebhook) m.Group("/{id}", func() { - m.Get("", repo.WebHooksEdit) - m.Post("/replay/{uuid}", repo.ReplayWebhook) + m.Get("", repo_setting.WebHooksEdit) + m.Post("/replay/{uuid}", repo_setting.ReplayWebhook) }) addWebhookEditRoutes() }, webhooksEnabled) @@ -752,8 +752,8 @@ func registerRoutes(m *web.Route) { m.Post("/delete", org.DeleteWebhook) addWebhookAddRoutes() m.Group("/{id}", func() { - m.Get("", repo.WebHooksEdit) - m.Post("/replay/{uuid}", repo.ReplayWebhook) + m.Get("", repo_setting.WebHooksEdit) + m.Post("/replay/{uuid}", repo_setting.ReplayWebhook) }) addWebhookEditRoutes() }, webhooksEnabled) @@ -874,78 +874,78 @@ func registerRoutes(m *web.Route) { m.Group("/{username}/{reponame}", func() { m.Group("/settings", func() { m.Group("", func() { - m.Combo("").Get(repo.Settings). - Post(web.Bind(forms.RepoSettingForm{}), repo.SettingsPost) - }, repo.SettingsCtxData) - m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo.SettingsAvatar) - m.Post("/avatar/delete", repo.SettingsDeleteAvatar) + m.Combo("").Get(repo_setting.Settings). + Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost) + }, repo_setting.SettingsCtxData) + m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar) + m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar) m.Group("/collaboration", func() { - m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost) - m.Post("/access_mode", repo.ChangeCollaborationAccessMode) - m.Post("/delete", repo.DeleteCollaboration) + m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost) + m.Post("/access_mode", repo_setting.ChangeCollaborationAccessMode) + m.Post("/delete", repo_setting.DeleteCollaboration) m.Group("/team", func() { - m.Post("", repo.AddTeamPost) - m.Post("/delete", repo.DeleteTeam) + m.Post("", repo_setting.AddTeamPost) + m.Post("/delete", repo_setting.DeleteTeam) }) }) m.Group("/branches", func() { - m.Post("/", repo.SetDefaultBranchPost) + m.Post("/", repo_setting.SetDefaultBranchPost) }, repo.MustBeNotEmpty) m.Group("/branches", func() { - m.Get("/", repo.ProtectedBranchRules) - m.Combo("/edit").Get(repo.SettingsProtectedBranch). - Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) - m.Post("/{id}/delete", repo.DeleteProtectedBranchRulePost) + m.Get("/", repo_setting.ProtectedBranchRules) + m.Combo("/edit").Get(repo_setting.SettingsProtectedBranch). + Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo_setting.SettingsProtectedBranchPost) + m.Post("/{id}/delete", repo_setting.DeleteProtectedBranchRulePost) }, repo.MustBeNotEmpty) - m.Post("/rename_branch", web.Bind(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost) + m.Post("/rename_branch", web.Bind(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo_setting.RenameBranchPost) m.Group("/tags", func() { - m.Get("", repo.Tags) - m.Post("", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.NewProtectedTagPost) - m.Post("/delete", context.RepoMustNotBeArchived(), repo.DeleteProtectedTagPost) - m.Get("/{id}", repo.EditProtectedTag) - m.Post("/{id}", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.EditProtectedTagPost) + m.Get("", repo_setting.ProtectedTags) + m.Post("", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo_setting.NewProtectedTagPost) + m.Post("/delete", context.RepoMustNotBeArchived(), repo_setting.DeleteProtectedTagPost) + m.Get("/{id}", repo_setting.EditProtectedTag) + m.Post("/{id}", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo_setting.EditProtectedTagPost) }) m.Group("/hooks/git", func() { - m.Get("", repo.GitHooks) - m.Combo("/{name}").Get(repo.GitHooksEdit). - Post(repo.GitHooksEditPost) + m.Get("", repo_setting.GitHooks) + m.Combo("/{name}").Get(repo_setting.GitHooksEdit). + Post(repo_setting.GitHooksEditPost) }, context.GitHookService()) m.Group("/hooks", func() { - m.Get("", repo.Webhooks) - m.Post("/delete", repo.DeleteWebhook) + m.Get("", repo_setting.Webhooks) + m.Post("/delete", repo_setting.DeleteWebhook) addWebhookAddRoutes() m.Group("/{id}", func() { - m.Get("", repo.WebHooksEdit) - m.Post("/test", repo.TestWebhook) - m.Post("/replay/{uuid}", repo.ReplayWebhook) + m.Get("", repo_setting.WebHooksEdit) + m.Post("/test", repo_setting.TestWebhook) + m.Post("/replay/{uuid}", repo_setting.ReplayWebhook) }) addWebhookEditRoutes() }, webhooksEnabled) m.Group("/keys", func() { - m.Combo("").Get(repo.DeployKeys). - Post(web.Bind(forms.AddKeyForm{}), repo.DeployKeysPost) - m.Post("/delete", repo.DeleteDeployKey) + m.Combo("").Get(repo_setting.DeployKeys). + Post(web.Bind(forms.AddKeyForm{}), repo_setting.DeployKeysPost) + m.Post("/delete", repo_setting.DeleteDeployKey) }) m.Group("/lfs", func() { - m.Get("/", repo.LFSFiles) - m.Get("/show/{oid}", repo.LFSFileGet) - m.Post("/delete/{oid}", repo.LFSDelete) - m.Get("/pointers", repo.LFSPointerFiles) - m.Post("/pointers/associate", repo.LFSAutoAssociate) - m.Get("/find", repo.LFSFileFind) + m.Get("/", repo_setting.LFSFiles) + m.Get("/show/{oid}", repo_setting.LFSFileGet) + m.Post("/delete/{oid}", repo_setting.LFSDelete) + m.Get("/pointers", repo_setting.LFSPointerFiles) + m.Post("/pointers/associate", repo_setting.LFSAutoAssociate) + m.Get("/find", repo_setting.LFSFileFind) m.Group("/locks", func() { - m.Get("/", repo.LFSLocks) - m.Post("/", repo.LFSLockFile) - m.Post("/{lid}/unlock", repo.LFSUnlock) + m.Get("/", repo_setting.LFSLocks) + m.Post("/", repo_setting.LFSLockFile) + m.Post("/{lid}/unlock", repo_setting.LFSUnlock) }) }) m.Group("/actions", func() { @@ -1037,7 +1037,7 @@ func registerRoutes(m *web.Route) { m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) - m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation) + m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.SetShowOutdatedComments, repo.UpdateResolveConversation) m.Post("/attachments", repo.UploadIssueAttachment) m.Post("/attachments/remove", repo.DeleteAttachment) m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin) @@ -1207,6 +1207,7 @@ func registerRoutes(m *web.Route) { Get(actions.View). Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne) + m.Get("/logs", actions.Logs) }) m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) m.Post("/approve", reqRepoActionsWriter, actions.Approve) @@ -1285,10 +1286,10 @@ func registerRoutes(m *web.Route) { m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) m.Group("/files", func() { - m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles) + m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFiles) m.Group("/reviews", func() { m.Get("/new_comment", repo.RenderNewCodeCommentForm) - m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.CreateCodeComment) + m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment) m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview) }, context.RepoMustNotBeArchived()) }) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 0616a5fc0dcb..d2893e4f23e0 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -56,12 +56,20 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error { return nil }); err != nil { log.Warn("Cannot stop task %v: %v", task.ID, err) - // go on - } else if remove, err := actions.TransferLogs(ctx, task.LogFilename); err != nil { + continue + } + + remove, err := actions.TransferLogs(ctx, task.LogFilename) + if err != nil { log.Warn("Cannot transfer logs of task %v: %v", task.ID, err) - } else { - remove() + continue + } + task.LogInStorage = true + if err := actions_model.UpdateTask(ctx, task, "log_in_storage"); err != nil { + log.Warn("Cannot update task %v: %v", task.ID, err) + continue } + remove() } CreateCommitStatus(ctx, jobs...) diff --git a/services/actions/notifier.go b/services/actions/notifier.go index da870bb84c8f..507eeaacf6cc 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -45,13 +45,13 @@ func (n *actionsNotifier) NotifyNewIssue(ctx context.Context, issue *issues_mode log.Error("issue.LoadPoster: %v", err) return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).WithPayload(&api.IssuePayload{ Action: api.HookIssueOpened, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, issue.Poster, nil), }).Notify(withMethod(ctx, "NotifyNewIssue")) } @@ -59,7 +59,7 @@ func (n *actionsNotifier) NotifyNewIssue(ctx context.Context, issue *issues_mode // NotifyIssueChangeStatus notifies close or reopen issue to notifiers func (n *actionsNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) { ctx = withMethod(ctx, "NotifyIssueChangeStatus") - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest: %v", err) @@ -69,7 +69,7 @@ func (n *actionsNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, PullRequest: convert.ToAPIPullRequest(db.DefaultContext, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, } @@ -88,7 +88,7 @@ func (n *actionsNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiIssue := &api.IssuePayload{ Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if isClosed { @@ -118,7 +118,7 @@ func (n *actionsNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { log.Error("loadPullRequest: %v", err) @@ -134,7 +134,7 @@ func (n *actionsNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, perm_model.AccessModeNone), + Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), }). WithPullRequest(issue.PullRequest). @@ -147,7 +147,7 @@ func (n *actionsNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }). Notify(ctx) @@ -159,7 +159,7 @@ func (n *actionsNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us ) { ctx = withMethod(ctx, "NotifyCreateIssueComment") - mode, _ := access_model.AccessLevel(ctx, doer, repo) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { @@ -172,7 +172,7 @@ func (n *actionsNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us Action: api.HookIssueCommentCreated, Issue: convert.ToAPIIssue(ctx, issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, repo, mode), + Repository: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: true, }). @@ -186,7 +186,7 @@ func (n *actionsNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us Action: api.HookIssueCommentCreated, Issue: convert.ToAPIIssue(ctx, issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, repo, mode), + Repository: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: false, }). @@ -209,14 +209,14 @@ func (n *actionsNotifier) NotifyNewPullRequest(ctx context.Context, pull *issues return } - mode, _ := access_model.AccessLevel(ctx, pull.Issue.Poster, pull.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, pull.Issue.Repo, pull.Issue.Poster) newNotifyInputFromIssue(pull.Issue, webhook_module.HookEventPullRequest). WithPayload(&api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pull, nil), - Repository: convert.ToRepo(ctx, pull.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission), Sender: convert.ToUser(ctx, pull.Issue.Poster, nil), }). WithPullRequest(pull). @@ -228,7 +228,7 @@ func (n *actionsNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u newNotifyInput(repo, doer, webhook_module.HookEventRepository).WithPayload(&api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) @@ -237,13 +237,13 @@ func (n *actionsNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u func (n *actionsNotifier) NotifyForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { ctx = withMethod(ctx, "NotifyForkRepository") - oldMode, _ := access_model.AccessLevel(ctx, doer, oldRepo) - mode, _ := access_model.AccessLevel(ctx, doer, repo) + oldPermission, _ := access_model.GetUserRepoPermission(ctx, oldRepo, doer) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) // forked webhook newNotifyInput(oldRepo, doer, webhook_module.HookEventFork).WithPayload(&api.ForkPayload{ - Forkee: convert.ToRepo(ctx, oldRepo, oldMode), - Repo: convert.ToRepo(ctx, repo, mode), + Forkee: convert.ToRepo(ctx, oldRepo, oldPermission), + Repo: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) @@ -255,7 +255,7 @@ func (n *actionsNotifier) NotifyForkRepository(ctx context.Context, doer *user_m WithRef(oldRepo.DefaultBranch). WithPayload(&api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) @@ -285,9 +285,9 @@ func (n *actionsNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue return } - mode, err := access_model.AccessLevel(ctx, review.Issue.Poster, review.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, review.Issue.Repo, review.Issue.Poster) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } @@ -297,7 +297,7 @@ func (n *actionsNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue Action: api.HookIssueReviewed, Index: review.Issue.Index, PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), - Repository: convert.ToRepo(ctx, review.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, review.Issue.Repo, permission), Sender: convert.ToUser(ctx, review.Reviewer, nil), Review: &api.ReviewPayload{ Type: string(reviewHookType), @@ -325,9 +325,9 @@ func (*actionsNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m return } - mode, err := access_model.AccessLevel(ctx, doer, pr.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, doer) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } @@ -335,7 +335,7 @@ func (*actionsNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m apiPullRequest := &api.PullRequestPayload{ Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(db.DefaultContext, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), Action: api.HookIssueClosed, } @@ -366,7 +366,7 @@ func (n *actionsNotifier) NotifyPushCommits(ctx context.Context, pusher *user_mo CompareURL: setting.AppURL + commits.CompareURL, Commits: apiCommits, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }). @@ -377,7 +377,7 @@ func (n *actionsNotifier) NotifyCreateRef(ctx context.Context, pusher *user_mode ctx = withMethod(ctx, "NotifyCreateRef") apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm_model.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}) newNotifyInput(repo, pusher, webhook_module.HookEventCreate). WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name @@ -395,7 +395,7 @@ func (n *actionsNotifier) NotifyDeleteRef(ctx context.Context, pusher *user_mode ctx = withMethod(ctx, "NotifyDeleteRef") apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm_model.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}) newNotifyInput(repo, pusher, webhook_module.HookEventDelete). WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name @@ -429,7 +429,7 @@ func (n *actionsNotifier) NotifySyncPushCommits(ctx context.Context, pusher *use Commits: apiCommits, TotalCommits: commits.Len, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }). @@ -494,7 +494,7 @@ func (n *actionsNotifier) NotifyPullRequestSynchronized(ctx context.Context, doe Action: api.HookIssueSynchronized, Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, perm_model.AccessModeNone), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), Sender: convert.ToUser(ctx, doer, nil), }). WithPullRequest(pr). @@ -514,7 +514,7 @@ func (n *actionsNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Contex return } - mode, _ := access_model.AccessLevel(ctx, pr.Issue.Poster, pr.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, pr.Issue.Poster) newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequest). WithPayload(&api.PullRequestPayload{ Action: api.HookIssueEdited, @@ -525,7 +525,7 @@ func (n *actionsNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Contex }, }, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }). WithPullRequest(pr). @@ -537,7 +537,7 @@ func (n *actionsNotifier) NotifyNewWikiPage(ctx context.Context, doer *user_mode newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ Action: api.HookWikiCreated, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -549,7 +549,7 @@ func (n *actionsNotifier) NotifyEditWikiPage(ctx context.Context, doer *user_mod newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ Action: api.HookWikiEdited, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -561,7 +561,7 @@ func (n *actionsNotifier) NotifyDeleteWikiPage(ctx context.Context, doer *user_m newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ Action: api.HookWikiDeleted, - Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, }).Notify(ctx) diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 5e41241d18f0..8e6cdcf680d1 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -142,13 +142,46 @@ func notify(ctx context.Context, input *notifyInput) error { return fmt.Errorf("gitRepo.GetCommit: %w", err) } + var detectedWorkflows []*actions_module.DetectedWorkflow workflows, err := actions_module.DetectWorkflows(commit, input.Event, input.Payload) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } - if len(workflows) == 0 { log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID) + } else { + for _, wf := range workflows { + if wf.TriggerEvent != actions_module.GithubEventPullRequestTarget { + wf.Ref = ref + detectedWorkflows = append(detectedWorkflows, wf) + } + } + } + + if input.PullRequest != nil { + // detect pull_request_target workflows + baseRef := git.BranchPrefix + input.PullRequest.BaseBranch + baseCommit, err := gitRepo.GetCommit(baseRef) + if err != nil { + return fmt.Errorf("gitRepo.GetCommit: %w", err) + } + baseWorkflows, err := actions_module.DetectWorkflows(baseCommit, input.Event, input.Payload) + if err != nil { + return fmt.Errorf("DetectWorkflows: %w", err) + } + if len(baseWorkflows) == 0 { + log.Trace("repo %s with commit %s couldn't find pull_request_target workflows", input.Repo.RepoPath(), baseCommit.ID) + } else { + for _, wf := range baseWorkflows { + if wf.TriggerEvent == actions_module.GithubEventPullRequestTarget { + wf.Ref = baseRef + detectedWorkflows = append(detectedWorkflows, wf) + } + } + } + } + + if len(detectedWorkflows) == 0 { return nil } @@ -172,18 +205,19 @@ func notify(ctx context.Context, input *notifyInput) error { } } - for id, content := range workflows { + for _, dwf := range detectedWorkflows { run := &actions_model.ActionRun{ Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0], RepoID: input.Repo.ID, OwnerID: input.Repo.OwnerID, - WorkflowID: id, + WorkflowID: dwf.EntryName, TriggerUserID: input.Doer.ID, - Ref: ref, - CommitSHA: commit.ID.String(), + Ref: dwf.Ref, + CommitSHA: dwf.Commit.ID.String(), IsForkPullRequest: isForkPullRequest, Event: input.Event, EventPayload: string(p), + TriggerEvent: dwf.TriggerEvent, Status: actions_model.StatusWaiting, } if need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer); err != nil { @@ -193,7 +227,7 @@ func notify(ctx context.Context, input *notifyInput) error { run.NeedApproval = need } - jobs, err := jobparser.Parse(content) + jobs, err := jobparser.Parse(dwf.Content) if err != nil { log.Error("jobparser.Parse: %v", err) continue @@ -222,14 +256,14 @@ func notifyRelease(ctx context.Context, doer *user_model.User, rel *repo_model.R return } - mode, _ := access_model.AccessLevel(ctx, doer, rel.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer) newNotifyInput(rel.Repo, doer, webhook_module.HookEventRelease). WithRef(git.RefNameFromTag(rel.TagName).String()). WithPayload(&api.ReleasePayload{ Action: action, Release: convert.ToRelease(ctx, rel), - Repository: convert.ToRepo(ctx, rel.Repo, mode), + Repository: convert.ToRepo(ctx, rel.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }). Notify(ctx) @@ -259,8 +293,10 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo } func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *repo_model.Repository, user *user_model.User) (bool, error) { - // don't need approval if it's not a fork PR - if !run.IsForkPullRequest { + // 1. don't need approval if it's not a fork PR + // 2. don't need approval if the event is `pull_request_target` since the workflow will run in the context of base branch + // see https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks + if !run.IsForkPullRequest || run.TriggerEvent == actions_module.GithubEventPullRequestTarget { return false, nil } diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 89e99b5e606c..3f3219adb946 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -76,7 +76,7 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str } if len(sr.Mail) == 0 { - sr.Mail = fmt.Sprintf("%s@localhost", sr.Username) + sr.Mail = fmt.Sprintf("%s@localhost.local", sr.Username) } user = &user_model.User{ diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 3e0f47a37e29..43ee32c84bdc 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -104,7 +104,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } if len(su.Mail) == 0 { - su.Mail = fmt.Sprintf("%s@localhost", su.Username) + su.Mail = fmt.Sprintf("%s@localhost.local", su.Username) } fullName := composeFullName(su.Name, su.Surname, su.Username) diff --git a/services/convert/activity.go b/services/convert/activity.go index 2aaa86607b7e..71a2722a493f 100644 --- a/services/convert/activity.go +++ b/services/convert/activity.go @@ -28,7 +28,7 @@ func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_mod ActUserID: ac.ActUserID, ActUser: ToUser(ctx, ac.ActUser, doer), RepoID: ac.RepoID, - Repo: ToRepo(ctx, ac.Repo, p.AccessMode), + Repo: ToRepo(ctx, ac.Repo, p), RefName: ac.RefName, IsPrivate: ac.IsPrivate, Content: ac.Content, diff --git a/services/convert/convert.go b/services/convert/convert.go index bce0e7ba214b..25c89747e304 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -50,7 +50,7 @@ func ToEmailSearch(email *user_model.SearchEmailResult) *api.Email { } // ToBranch convert a git.Commit and git.Branch to an api.Branch -func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) { +func ToBranch(ctx context.Context, repo *repo_model.Repository, branchName string, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) { if bp == nil { var hasPerm bool var canPush bool @@ -65,11 +65,11 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c if err != nil { return nil, err } - canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user) + canPush = issues_model.CanMaintainerWriteToBranch(perms, branchName, user) } return &api.Branch{ - Name: b.Name, + Name: branchName, Commit: ToPayloadCommit(ctx, repo, c), Protected: false, RequiredApprovals: 0, @@ -81,7 +81,7 @@ func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c } branch := &api.Branch{ - Name: b.Name, + Name: branchName, Commit: ToPayloadCommit(ctx, repo, c), Protected: true, RequiredApprovals: bp.RequiredApprovals, diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go index 2810c6c9bc91..db48faa69e69 100644 --- a/services/convert/issue_comment.go +++ b/services/convert/issue_comment.go @@ -11,6 +11,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) // ToComment converts a issues_model.Comment to the api.Comment format @@ -66,6 +67,17 @@ func ToTimelineComment(ctx context.Context, c *issues_model.Comment, doer *user_ return nil } + if c.Content != "" { + if (c.Type == issues_model.CommentTypeAddTimeManual || + c.Type == issues_model.CommentTypeStopTracking || + c.Type == issues_model.CommentTypeDeleteTimeManual) && + c.Content[0] == '|' { + // TimeTracking Comments from v1.21 on store the seconds instead of an formated string + // so we check for the "|" delimeter and convert new to legacy format on demand + c.Content = util.SecToTime(c.Content[1:]) + } + } + comment := &api.TimelineComment{ ID: c.ID, Type: c.Type.String(), diff --git a/services/convert/notification.go b/services/convert/notification.go index 5d3b078a25d5..3906fa9b388a 100644 --- a/services/convert/notification.go +++ b/services/convert/notification.go @@ -9,6 +9,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" api "code.gitea.io/gitea/modules/structs" ) @@ -24,7 +25,7 @@ func ToNotificationThread(n *activities_model.Notification) *api.NotificationThr // since user only get notifications when he has access to use minimal access mode if n.Repository != nil { - result.Repository = ToRepo(db.DefaultContext, n.Repository, perm.AccessModeRead) + result.Repository = ToRepo(db.DefaultContext, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead}) // This permission is not correct and we should not be reporting it for repository := result.Repository; repository != nil; repository = repository.Parent { diff --git a/services/convert/package.go b/services/convert/package.go index 7d170ccc253d..276856594bb4 100644 --- a/services/convert/package.go +++ b/services/convert/package.go @@ -22,7 +22,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m } if permission.HasAccess() { - repo = ToRepo(ctx, pd.Repository, permission.AccessMode) + repo = ToRepo(ctx, pd.Repository, permission) } } diff --git a/services/convert/pull.go b/services/convert/pull.go index 1ac0f4e96f1e..e4e3097056a2 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -80,7 +80,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u Name: pr.BaseBranch, Ref: pr.BaseBranch, RepoID: pr.BaseRepoID, - Repository: ToRepo(ctx, pr.BaseRepo, p.AccessMode), + Repository: ToRepo(ctx, pr.BaseRepo, p), }, Head: &api.PRBranchInfo{ Name: pr.HeadBranch, @@ -152,7 +152,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u } apiPullRequest.Head.RepoID = pr.HeadRepo.ID - apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p.AccessMode) + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath()) if err != nil { diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go index 0915d096e66c..e069fa4a68bf 100644 --- a/services/convert/pull_test.go +++ b/services/convert/pull_test.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" @@ -31,7 +32,7 @@ func TestPullRequest_APIFormat(t *testing.T) { Ref: "refs/pull/2/head", Sha: "4a357436d925b5c974181ff12a994538ddc5a269", RepoID: 1, - Repository: ToRepo(db.DefaultContext, headRepo, perm.AccessModeRead), + Repository: ToRepo(db.DefaultContext, headRepo, access_model.Permission{AccessMode: perm.AccessModeRead}), }, apiPullRequest.Head) // withOut HeadRepo diff --git a/services/convert/repository.go b/services/convert/repository.go index 54a61efe43d5..6f77b4932e4f 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" @@ -16,18 +17,26 @@ import ( ) // ToRepo converts a Repository to api.Repository -func ToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode) *api.Repository { - return innerToRepo(ctx, repo, mode, false) +func ToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission) *api.Repository { + return innerToRepo(ctx, repo, permissionInRepo, false) } -func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode, isParent bool) *api.Repository { +func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository { var parent *api.Repository + if permissionInRepo.Units == nil && permissionInRepo.UnitsMode == nil { + // If Units and UnitsMode are both nil, it means that it's a hard coded permission, + // like access_model.Permission{AccessMode: perm.AccessModeAdmin}. + // So we need to load units for the repo, or UnitAccessMode will always return perm.AccessModeNone. + _ = repo.LoadUnits(ctx) // the error is not important, so ignore it + permissionInRepo.Units = repo.Units + } + cloneLink := repo.CloneLink() permission := &api.Permission{ - Admin: mode >= perm.AccessModeAdmin, - Push: mode >= perm.AccessModeWrite, - Pull: mode >= perm.AccessModeRead, + Admin: permissionInRepo.AccessMode >= perm.AccessModeAdmin, + Push: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeWrite, + Pull: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead, } if !isParent { err := repo.GetBaseRepo(ctx) @@ -35,7 +44,12 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc return nil } if repo.BaseRepo != nil { - parent = innerToRepo(ctx, repo.BaseRepo, mode, true) + // FIXME: The permission of the parent repo is not correct. + // It's the permission of the current repo, so it's probably different from the parent repo. + // But there isn't a good way to get the permission of the parent repo, because the doer is not passed in. + // Use the permission of the current repo to keep the behavior consistent with the old API. + // Maybe the right way is setting the permission of the parent repo to nil, empty is better than wrong. + parent = innerToRepo(ctx, repo.BaseRepo, permissionInRepo, true) } } @@ -154,7 +168,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc return &api.Repository{ ID: repo.ID, - Owner: ToUserWithAccessMode(ctx, repo.Owner, mode), + Owner: ToUserWithAccessMode(ctx, repo.Owner, permissionInRepo.AccessMode), Name: repo.Name, FullName: repo.FullName(), Description: repo.Description, diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 0a4e2729e7b5..1f5abf94ee12 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -27,7 +27,6 @@ type InstallForm struct { DbPasswd string DbName string SSLMode string - Charset string `binding:"Required;In(utf8,utf8mb4)"` DbPath string DbSchema string diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index b6a75f60982f..9adf3b940093 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -450,8 +450,8 @@ type Diff struct { } // LoadComments loads comments into each line -func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User) error { - allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser) +func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error { + allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser, showOutdatedComments) if err != nil { return err } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 389f787dfc44..e270e46fd453 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -594,16 +594,26 @@ func setupDefaultDiff() *Diff { } } -func TestDiff_LoadComments(t *testing.T) { +func TestDiff_LoadCommentsNoOutdated(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}) diff := setupDefaultDiff() - assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user)) + assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, false)) assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2) } +func TestDiff_LoadCommentsWithOutdated(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}) + diff := setupDefaultDiff() + assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user, true)) + assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 3) +} + func TestDiffLine_CanComment(t *testing.T) { assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment()) assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment()) diff --git a/services/lfs/server.go b/services/lfs/server.go index a18e752d4791..b32f218785ed 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -77,6 +77,8 @@ func CheckAcceptMediaType(ctx *context.Context) { } } +var rangeHeaderRegexp = regexp.MustCompile(`bytes=(\d+)\-(\d*).*`) + // DownloadHandler gets the content from the content store func DownloadHandler(ctx *context.Context) { rc := getRequestContext(ctx) @@ -92,8 +94,7 @@ func DownloadHandler(ctx *context.Context) { toByte = meta.Size - 1 statusCode := http.StatusOK if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" { - regex := regexp.MustCompile(`bytes=(\d+)\-(\d*).*`) - match := regex.FindStringSubmatch(rangeHdr) + match := rangeHeaderRegexp.FindStringSubmatch(rangeHdr) if len(match) > 1 { statusCode = http.StatusPartialContent fromByte, _ = strconv.ParseInt(match[1], 10, 32) diff --git a/services/migrations/dump.go b/services/migrations/dump.go index cc8518d4a25c..729112bcd239 100644 --- a/services/migrations/dump.go +++ b/services/migrations/dump.go @@ -642,7 +642,7 @@ func (g *RepositoryDumper) Finish() error { // DumpRepository dump repository according MigrateOptions to a local directory func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error { - doer, err := user_model.GetAdminUser() + doer, err := user_model.GetAdminUser(ctx) if err != nil { return err } @@ -705,7 +705,7 @@ func updateOptionsUnits(opts *base.MigrateOptions, units []string) error { // RestoreRepository restore a repository from the disk directory func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string, validation bool) error { - doer, err := user_model.GetAdminUser() + doer, err := user_model.GetAdminUser(ctx) if err != nil { return err } diff --git a/services/pull/pull.go b/services/pull/pull.go index f44e690ab708..0f562b9ee35d 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -170,7 +170,7 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer return err } if branchesEqual { - return models.ErrBranchesEqual{ + return git_model.ErrBranchesEqual{ HeadBranchName: pr.HeadBranch, BaseBranchName: targetBranch, } @@ -338,7 +338,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, for _, pr := range prs { divergence, err := GetDiverging(ctx, pr) if err != nil { - if models.IsErrBranchDoesNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { + if git_model.IsErrBranchNotExist(err) && !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch) } else { log.Error("GetDiverging: %v", err) diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 146470780671..db32940e3835 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -11,7 +11,7 @@ import ( "path/filepath" "strings" - "code.gitea.io/gitea/models" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" @@ -181,7 +181,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) Run(prCtx.RunOpts()); err != nil { cancel() if !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { - return nil, nil, models.ErrBranchDoesNotExist{ + return nil, nil, git_model.ErrBranchNotExist{ BranchName: pr.HeadBranch, } } diff --git a/services/pull/update.go b/services/pull/update.go index b977dbdba9fe..bc8c4a25e5f6 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "code.gitea.io/gitea/models" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" @@ -168,7 +167,7 @@ func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.Diver log.Trace("GetDiverging[%-v]: compare commits", pr) prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) if err != nil { - if !models.IsErrBranchDoesNotExist(err) { + if !git_model.IsErrBranchNotExist(err) { log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) } return nil, err diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 55e77a78a78a..f95fb5988f66 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -12,6 +12,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -70,9 +71,17 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts repo_mo if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil { return err } - if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil { + + // Re-fetch the repository from database before updating it (else it would + // override changes that were done earlier with sql) + if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { + return fmt.Errorf("getRepositoryByID: %w", err) + } + + if err := adoptRepository(ctx, repoPath, doer, repo, opts.DefaultBranch); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } + if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { return fmt.Errorf("checkDaemonExportOK: %w", err) } @@ -95,12 +104,12 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts repo_mo return nil, err } - notification.NotifyCreateRepository(ctx, doer, u, repo) + notification.NotifyAdoptRepository(ctx, doer, u, repo) return repo, nil } -func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts repo_module.CreateRepoOptions) (err error) { +func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, defaultBranch string) (err error) { isExist, err := util.IsExist(repoPath) if err != nil { log.Error("Unable to check if %s exists. Error: %v", repoPath, err) @@ -114,12 +123,6 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r return fmt.Errorf("createDelegateHooks: %w", err) } - // Re-fetch the repository from database before updating it (else it would - // override changes that were done earlier with sql) - if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { - return fmt.Errorf("getRepositoryByID: %w", err) - } - repo.IsEmpty = false // Don't bother looking this repo in the context it won't be there @@ -129,8 +132,8 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r } defer gitRepo.Close() - if len(opts.DefaultBranch) > 0 { - repo.DefaultBranch = opts.DefaultBranch + if len(defaultBranch) > 0 { + repo.DefaultBranch = defaultBranch if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) @@ -144,7 +147,15 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r } } } - branches, _, _ := gitRepo.GetBranchNames(0, 0) + + branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ + RepoID: repo.ID, + ListOptions: db.ListOptions{ + ListAll: true, + }, + IsDeletedBranch: util.OptionalBoolFalse, + }) + found := false hasDefault := false hasMaster := false diff --git a/services/repository/branch.go b/services/repository/branch.go index 4e560786dbc5..11a8b2053157 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -10,13 +10,21 @@ import ( "strings" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" 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" "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/queue" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/util" + files_service "code.gitea.io/gitea/services/repository/files" + + "xorm.io/builder" ) // CreateNewBranch creates a new repository branch @@ -27,7 +35,7 @@ func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_mode } if !git.IsBranchExist(ctx, repo.RepoPath(), oldBranchName) { - return models.ErrBranchDoesNotExist{ + return git_model.ErrBranchNotExist{ BranchName: oldBranchName, } } @@ -40,16 +48,165 @@ func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_mode if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { return err } - return fmt.Errorf("Push: %w", err) + return fmt.Errorf("push: %w", err) } return nil } -// GetBranches returns branches from the repository, skipping skip initial branches and -// returning at most limit branches, or all branches if limit is 0. -func GetBranches(ctx context.Context, repo *repo_model.Repository, skip, limit int) ([]*git.Branch, int, error) { - return git.GetBranchesByPath(ctx, repo.RepoPath(), skip, limit) +// Branch contains the branch information +type Branch struct { + DBBranch *git_model.Branch + IsProtected bool + IsIncluded bool + CommitsAhead int + CommitsBehind int + LatestPullRequest *issues_model.PullRequest + MergeMovedOn bool +} + +// LoadBranches loads branches from the repository limited by page & pageSize. +func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch util.OptionalBool, page, pageSize int) (*Branch, []*Branch, int64, error) { + defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch) + if err != nil { + return nil, nil, 0, err + } + + branchOpts := git_model.FindBranchOptions{ + RepoID: repo.ID, + IsDeletedBranch: isDeletedBranch, + ListOptions: db.ListOptions{ + Page: page, + PageSize: pageSize, + }, + } + + totalNumOfBranches, err := git_model.CountBranches(ctx, branchOpts) + if err != nil { + return nil, nil, 0, err + } + + branchOpts.ExcludeBranchNames = []string{repo.DefaultBranch} + + dbBranches, err := git_model.FindBranches(ctx, branchOpts) + if err != nil { + return nil, nil, 0, err + } + + if err := dbBranches.LoadDeletedBy(ctx); err != nil { + return nil, nil, 0, err + } + if err := dbBranches.LoadPusher(ctx); err != nil { + return nil, nil, 0, err + } + + rules, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID) + if err != nil { + return nil, nil, 0, err + } + + repoIDToRepo := map[int64]*repo_model.Repository{} + repoIDToRepo[repo.ID] = repo + + repoIDToGitRepo := map[int64]*git.Repository{} + repoIDToGitRepo[repo.ID] = gitRepo + + branches := make([]*Branch, 0, len(dbBranches)) + for i := range dbBranches { + branch, err := loadOneBranch(ctx, repo, dbBranches[i], &rules, repoIDToRepo, repoIDToGitRepo) + if err != nil { + return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) + } + + branches = append(branches, branch) + } + + // Always add the default branch + log.Debug("loadOneBranch: load default: '%s'", defaultDBBranch.Name) + defaultBranch, err := loadOneBranch(ctx, repo, defaultDBBranch, &rules, repoIDToRepo, repoIDToGitRepo) + if err != nil { + return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) + } + + return defaultBranch, branches, totalNumOfBranches, nil +} + +func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules, + repoIDToRepo map[int64]*repo_model.Repository, + repoIDToGitRepo map[int64]*git.Repository, +) (*Branch, error) { + log.Trace("loadOneBranch: '%s'", dbBranch.Name) + + branchName := dbBranch.Name + p := protectedBranches.GetFirstMatched(branchName) + isProtected := p != nil + + divergence := &git.DivergeObject{ + Ahead: -1, + Behind: -1, + } + + // it's not default branch + if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted { + var err error + divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName) + if err != nil { + log.Error("CountDivergingCommits: %v", err) + } + } + + pr, err := issues_model.GetLatestPullRequestByHeadInfo(repo.ID, branchName) + if err != nil { + return nil, fmt.Errorf("GetLatestPullRequestByHeadInfo: %v", err) + } + headCommit := dbBranch.CommitID + + mergeMovedOn := false + if pr != nil { + pr.HeadRepo = repo + if err := pr.LoadIssue(ctx); err != nil { + return nil, fmt.Errorf("LoadIssue: %v", err) + } + if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok { + pr.BaseRepo = repo + } else if err := pr.LoadBaseRepo(ctx); err != nil { + return nil, fmt.Errorf("LoadBaseRepo: %v", err) + } else { + repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo + } + pr.Issue.Repo = pr.BaseRepo + + if pr.HasMerged { + baseGitRepo, ok := repoIDToGitRepo[pr.BaseRepoID] + if !ok { + baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) + if err != nil { + return nil, fmt.Errorf("OpenRepository: %v", err) + } + defer baseGitRepo.Close() + repoIDToGitRepo[pr.BaseRepoID] = baseGitRepo + } + pullCommit, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) + if err != nil && !git.IsErrNotExist(err) { + return nil, fmt.Errorf("GetBranchCommitID: %v", err) + } + if err == nil && headCommit != pullCommit { + // the head has moved on from the merge - we shouldn't delete + mergeMovedOn = true + } + } + } + + isIncluded := divergence.Ahead == 0 && repo.DefaultBranch != branchName + return &Branch{ + DBBranch: dbBranch, + IsProtected: isProtected, + IsIncluded: isIncluded, + CommitsAhead: divergence.Ahead, + CommitsBehind: divergence.Behind, + LatestPullRequest: pr, + MergeMovedOn: mergeMovedOn, + }, nil } func GetBranchCommitID(ctx context.Context, repo *repo_model.Repository, branch string) (string, error) { @@ -62,17 +219,17 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri branchRefName := strings.TrimPrefix(refName, git.BranchPrefix) switch { case branchRefName == name: - return models.ErrBranchAlreadyExists{ + return git_model.ErrBranchAlreadyExists{ BranchName: name, } // If branchRefName like a/b but we want to create a branch named a then we have a conflict case strings.HasPrefix(branchRefName, name+"/"): - return models.ErrBranchNameConflict{ + return git_model.ErrBranchNameConflict{ BranchName: branchRefName, } // Conversely if branchRefName like a but we want to create a branch named a/b then we also have a conflict case strings.HasPrefix(name, branchRefName+"/"): - return models.ErrBranchNameConflict{ + return git_model.ErrBranchNameConflict{ BranchName: branchRefName, } case refName == git.TagPrefix+name: @@ -101,7 +258,7 @@ func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { return err } - return fmt.Errorf("Push: %w", err) + return fmt.Errorf("push: %w", err) } return nil @@ -169,13 +326,28 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R return git_model.ErrBranchIsProtected } + rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName) + if err != nil { + return fmt.Errorf("GetBranch: %vc", err) + } + + if rawBranch.IsDeleted { + return nil + } + commit, err := gitRepo.GetBranchCommit(branchName) if err != nil { return err } - if err := gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ - Force: true, + if err := db.WithTx(ctx, func(ctx context.Context) error { + if err := git_model.AddDeletedBranch(ctx, repo.ID, branchName, doer.ID); err != nil { + return err + } + + return gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{ + Force: true, + }) }); err != nil { return err } @@ -196,3 +368,45 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R return nil } + +type BranchSyncOptions struct { + RepoID int64 +} + +// branchSyncQueue represents a queue to handle branch sync jobs. +var branchSyncQueue *queue.WorkerPoolQueue[*BranchSyncOptions] + +func handlerBranchSync(items ...*BranchSyncOptions) []*BranchSyncOptions { + for _, opts := range items { + _, err := repo_module.SyncRepoBranches(graceful.GetManager().ShutdownContext(), opts.RepoID, 0) + if err != nil { + log.Error("syncRepoBranches [%d] failed: %v", opts.RepoID, err) + } + } + return nil +} + +func addRepoToBranchSyncQueue(repoID, doerID int64) error { + return branchSyncQueue.Push(&BranchSyncOptions{ + RepoID: repoID, + }) +} + +func initBranchSyncQueue(ctx context.Context) error { + branchSyncQueue = queue.CreateUniqueQueue(ctx, "branch_sync", handlerBranchSync) + if branchSyncQueue == nil { + return errors.New("unable to create branch_sync queue") + } + go graceful.GetManager().RunWithCancel(branchSyncQueue) + + return nil +} + +func AddAllRepoBranchesToSyncQueue(ctx context.Context, doerID int64) error { + if err := db.Iterate(ctx, builder.Eq{"is_empty": false}, func(ctx context.Context, repo *repo_model.Repository) error { + return addRepoToBranchSyncQueue(repo.ID, doerID) + }); err != nil { + return fmt.Errorf("run sync all branches failed: %v", err) + } + return nil +} diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index 19d089b9e49c..fdf0b32f1a1d 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -58,7 +58,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode if opts.NewBranch != opts.OldBranch { existingBranch, err := gitRepo.GetBranch(opts.NewBranch) if existingBranch != nil { - return models.ErrBranchAlreadyExists{ + return git_model.ErrBranchAlreadyExists{ BranchName: opts.NewBranch, } } diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 01bf2ace0093..737f914dd684 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -197,7 +197,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use if opts.NewBranch != opts.OldBranch { existingBranch, err := gitRepo.GetBranch(opts.NewBranch) if existingBranch != nil { - return nil, models.ErrBranchAlreadyExists{ + return nil, git_model.ErrBranchAlreadyExists{ BranchName: opts.NewBranch, } } diff --git a/services/repository/fork.go b/services/repository/fork.go index fb93b10f1c31..59aa17337331 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -157,7 +157,15 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork if err = repo_module.CreateDelegateHooks(repoPath); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } - return nil + + gitRepo, err := git.OpenRepository(txCtx, repo.RepoPath()) + if err != nil { + return fmt.Errorf("OpenRepository: %w", err) + } + defer gitRepo.Close() + + _, err = repo_module.SyncRepoBranchesWithRepo(txCtx, repo, gitRepo, doer.ID) + return err }) needsRollbackInPanic = false if err != nil { diff --git a/services/repository/push.go b/services/repository/push.go index e559d3f904cb..8e4bab156293 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -93,7 +93,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { defer gitRepo.Close() if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { - log.Error("Failed to update size for repository: %v", err) + return fmt.Errorf("Failed to update size for repository: %v", err) } addTags := make([]string, 0, len(optsList)) @@ -259,8 +259,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { notification.NotifyPushCommits(ctx, pusher, repo, opts, commits) - if err = git_model.RemoveDeletedBranchByName(ctx, repo.ID, branch); err != nil { - log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err) + if err = git_model.UpdateBranch(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil { + return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err) } // Cache for big repository @@ -273,8 +273,9 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { // close all related pulls log.Error("close related pull request failed: %v", err) } - if err := git_model.AddDeletedBranch(db.DefaultContext, repo.ID, branch, opts.OldCommitID, pusher.ID); err != nil { - log.Warn("AddDeletedBranch: %v", err) + + if err := git_model.AddDeletedBranch(db.DefaultContext, repo.ID, branch, pusher.ID); err != nil { + return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err) } } diff --git a/services/repository/repository.go b/services/repository/repository.go index 0914a8f6ec6a..cd3658dcd8e0 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -17,6 +17,7 @@ import ( system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" repo_module "code.gitea.io/gitea/modules/repository" @@ -100,7 +101,10 @@ func Init() error { } system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath) system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) - return initPushQueue() + if err := initPushQueue(); err != nil { + return err + } + return initBranchSyncQueue(graceful.GetManager().ShutdownContext()) } // UpdateRepository updates a repository diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index bccd477852b7..3332d5d4aa1e 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -50,7 +49,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { @@ -62,7 +61,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user Action: api.HookIssueLabelCleared, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -70,7 +69,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user Action: api.HookIssueLabelCleared, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -80,13 +79,13 @@ func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user } func (m *webhookNotifier) NotifyForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) { - oldMode, _ := access_model.AccessLevel(ctx, doer, oldRepo) - mode, _ := access_model.AccessLevel(ctx, doer, repo) + oldPermission, _ := access_model.GetUserRepoPermission(ctx, oldRepo, doer) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) // forked webhook if err := PrepareWebhooks(ctx, EventSource{Repository: oldRepo}, webhook_module.HookEventFork, &api.ForkPayload{ - Forkee: convert.ToRepo(ctx, oldRepo, oldMode), - Repo: convert.ToRepo(ctx, repo, mode), + Forkee: convert.ToRepo(ctx, oldRepo, oldPermission), + Repo: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err) @@ -98,7 +97,7 @@ func (m *webhookNotifier) NotifyForkRepository(ctx context.Context, doer *user_m if u.IsOrganization() { if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -111,7 +110,7 @@ func (m *webhookNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u // Add to hook queue for created repo after session commit. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -122,7 +121,7 @@ func (m *webhookNotifier) NotifyCreateRepository(ctx context.Context, doer, u *u func (m *webhookNotifier) NotifyDeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) { if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoDeleted, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, repo.MustOwner(ctx), nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -134,7 +133,7 @@ func (m *webhookNotifier) NotifyMigrateRepository(ctx context.Context, doer, u * // Add to hook queue for created repo after session commit. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Organization: convert.ToUser(ctx, u, nil), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { @@ -144,7 +143,7 @@ func (m *webhookNotifier) NotifyMigrateRepository(ctx context.Context, doer, u * func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { if issue.IsPull { - mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypePullRequests) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest failed: %v", err) @@ -153,7 +152,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *u apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if removed { @@ -167,11 +166,11 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *u return } } else { - mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypeIssues) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) apiIssue := &api.IssuePayload{ Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if removed { @@ -188,7 +187,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *u } func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) { - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { @@ -204,7 +203,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user }, }, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -217,7 +216,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user }, }, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -228,7 +227,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user } func (m *webhookNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { @@ -239,7 +238,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiPullRequest := &api.PullRequestPayload{ Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, } @@ -253,7 +252,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *use apiIssue := &api.IssuePayload{ Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), CommitID: commitID, } @@ -279,12 +278,12 @@ func (m *webhookNotifier) NotifyNewIssue(ctx context.Context, issue *issues_mode return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueOpened, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, issue.Poster, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) @@ -305,12 +304,12 @@ func (m *webhookNotifier) NotifyNewPullRequest(ctx context.Context, pull *issues return } - mode, _ := access_model.AccessLevel(ctx, pull.Issue.Poster, pull.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, pull.Issue.Repo, pull.Issue.Poster) if err := PrepareWebhooks(ctx, EventSource{Repository: pull.Issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pull, nil), - Repository: convert.ToRepo(ctx, pull.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission), Sender: convert.ToUser(ctx, pull.Issue.Poster, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) @@ -323,7 +322,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *us return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) var err error if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { @@ -339,7 +338,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *us }, }, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -352,7 +351,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *us }, }, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -383,7 +382,7 @@ func (m *webhookNotifier) NotifyUpdateComment(ctx context.Context, doer *user_mo eventType = webhook_module.HookEventIssueComment } - mode, _ := access_model.AccessLevel(ctx, doer, c.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, c.Issue.Repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: c.Issue.Repo}, eventType, &api.IssueCommentPayload{ Action: api.HookIssueCommentEdited, Issue: convert.ToAPIIssue(ctx, c.Issue), @@ -393,7 +392,7 @@ func (m *webhookNotifier) NotifyUpdateComment(ctx context.Context, doer *user_mo From: oldContent, }, }, - Repository: convert.ToRepo(ctx, c.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, c.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: c.Issue.IsPull, }); err != nil { @@ -411,12 +410,12 @@ func (m *webhookNotifier) NotifyCreateIssueComment(ctx context.Context, doer *us eventType = webhook_module.HookEventIssueComment } - mode, _ := access_model.AccessLevel(ctx, doer, repo) + permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, eventType, &api.IssueCommentPayload{ Action: api.HookIssueCommentCreated, Issue: convert.ToAPIIssue(ctx, issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, repo, mode), + Repository: convert.ToRepo(ctx, repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: issue.IsPull, }); err != nil { @@ -448,12 +447,12 @@ func (m *webhookNotifier) NotifyDeleteComment(ctx context.Context, doer *user_mo eventType = webhook_module.HookEventIssueComment } - mode, _ := access_model.AccessLevel(ctx, doer, comment.Issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: comment.Issue.Repo}, eventType, &api.IssueCommentPayload{ Action: api.HookIssueCommentDeleted, Issue: convert.ToAPIIssue(ctx, comment.Issue), Comment: convert.ToComment(ctx, comment), - Repository: convert.ToRepo(ctx, comment.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), IsPull: comment.Issue.IsPull, }); err != nil { @@ -465,7 +464,7 @@ func (m *webhookNotifier) NotifyNewWikiPage(ctx context.Context, doer *user_mode // Add to hook queue for created wiki page. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiCreated, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -478,7 +477,7 @@ func (m *webhookNotifier) NotifyEditWikiPage(ctx context.Context, doer *user_mod // Add to hook queue for edit wiki page. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiEdited, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, Comment: comment, @@ -491,7 +490,7 @@ func (m *webhookNotifier) NotifyDeleteWikiPage(ctx context.Context, doer *user_m // Add to hook queue for edit wiki page. if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiDeleted, - Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), Page: page, }); err != nil { @@ -514,7 +513,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use return } - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { log.Error("loadPullRequest: %v", err) @@ -528,7 +527,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, perm.AccessModeNone), + Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -536,7 +535,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *use Action: api.HookIssueLabelUpdated, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -559,7 +558,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(ctx context.Context, doer * return } - mode, _ := access_model.AccessLevel(ctx, doer, issue.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if issue.IsPull { err = issue.PullRequest.LoadIssue(ctx) if err != nil { @@ -570,7 +569,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(ctx context.Context, doer * Action: hookAction, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } else { @@ -578,7 +577,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(ctx context.Context, doer * Action: hookAction, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }) } @@ -603,7 +602,7 @@ func (m *webhookNotifier) NotifyPushCommits(ctx context.Context, pusher *user_mo Commits: apiCommits, TotalCommits: commits.Len, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }); err != nil { @@ -633,9 +632,9 @@ func (*webhookNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m return } - mode, err := access_model.AccessLevel(ctx, doer, pr.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, doer) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } @@ -643,7 +642,7 @@ func (*webhookNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_m apiPullRequest := &api.PullRequestPayload{ Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), Action: api.HookIssueClosed, } @@ -661,7 +660,7 @@ func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Contex issue := pr.Issue - mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo) + mode, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, @@ -699,16 +698,16 @@ func (m *webhookNotifier) NotifyPullRequestReview(ctx context.Context, pr *issue return } - mode, err := access_model.AccessLevel(ctx, review.Issue.Poster, review.Issue.Repo) + permission, err := access_model.GetUserRepoPermission(ctx, review.Issue.Repo, review.Issue.Poster) if err != nil { - log.Error("models.AccessLevel: %v", err) + log.Error("models.GetUserRepoPermission: %v", err) return } if err := PrepareWebhooks(ctx, EventSource{Repository: review.Issue.Repo}, reviewHookType, &api.PullRequestPayload{ Action: api.HookIssueReviewed, Index: review.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, review.Issue.Repo, mode), + Repository: convert.ToRepo(ctx, review.Issue.Repo, permission), Sender: convert.ToUser(ctx, review.Reviewer, nil), Review: &api.ReviewPayload{ Type: string(reviewHookType), @@ -724,7 +723,7 @@ func (m *webhookNotifier) NotifyPullRequestReviewRequest(ctx context.Context, do log.Warn("NotifyPullRequestReviewRequest: issue is not a pull request: %v", issue.ID) return } - mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypePullRequests) + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) if err := issue.LoadPullRequest(ctx); err != nil { log.Error("LoadPullRequest failed: %v", err) return @@ -733,7 +732,7 @@ func (m *webhookNotifier) NotifyPullRequestReviewRequest(ctx context.Context, do Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), RequestedReviewer: convert.ToUser(ctx, reviewer, nil), - Repository: convert.ToRepo(ctx, issue.Repo, mode), + Repository: convert.ToRepo(ctx, issue.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), } if isRequest { @@ -749,7 +748,7 @@ func (m *webhookNotifier) NotifyPullRequestReviewRequest(ctx context.Context, do func (m *webhookNotifier) NotifyCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}) refName := refFullName.ShortName() if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventCreate, &api.CreatePayload{ @@ -777,7 +776,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(ctx context.Context, doe Action: api.HookIssueSynchronized, Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), - Repository: convert.ToRepo(ctx, pr.Issue.Repo, perm.AccessModeNone), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err) @@ -786,7 +785,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(ctx context.Context, doe func (m *webhookNotifier) NotifyDeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { apiPusher := convert.ToUser(ctx, pusher, nil) - apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone) + apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}) refName := refFullName.ShortName() if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventDelete, &api.DeletePayload{ @@ -806,11 +805,11 @@ func sendReleaseHook(ctx context.Context, doer *user_model.User, rel *repo_model return } - mode, _ := access_model.AccessLevel(ctx, doer, rel.Repo) + permission, _ := access_model.GetUserRepoPermission(ctx, rel.Repo, doer) if err := PrepareWebhooks(ctx, EventSource{Repository: rel.Repo}, webhook_module.HookEventRelease, &api.ReleasePayload{ Action: action, Release: convert.ToRelease(ctx, rel), - Repository: convert.ToRepo(ctx, rel.Repo, mode), + Repository: convert.ToRepo(ctx, rel.Repo, permission), Sender: convert.ToUser(ctx, doer, nil), }); err != nil { log.Error("PrepareWebhooks: %v", err) @@ -845,7 +844,7 @@ func (m *webhookNotifier) NotifySyncPushCommits(ctx context.Context, pusher *use Commits: apiCommits, TotalCommits: commits.Len, HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(ctx, repo, perm.AccessModeOwner), + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Pusher: apiPusher, Sender: apiPusher, }); err != nil { diff --git a/templates/admin/base/search.tmpl b/templates/admin/base/search.tmpl index ae1a4d2ac562..19977f05a9bc 100644 --- a/templates/admin/base/search.tmpl +++ b/templates/admin/base/search.tmpl @@ -1,6 +1,12 @@ -